Java · free · no signup

Learn java, with practice after every lesson

10 lessons, about 170 minutes of reading, and 30 multiple-choice questions. Each lesson names the mistake that most often costs people the interview, because that is where the hours actually go. Part of the free coding course.

Types, classes and main

Basics Java · 15 min · 15 XP

Java requires you to declare the type of everything, and all code lives inside a class. That feels heavy at first, but it is exactly why large teams and banks use it: the compiler catches whole categories of mistake before the program ever runs, which matters more when fifty people share a codebase than when one person writes a script.

Every Java program starts at a main method. Enterprise employers — especially in banking, insurance and large-scale backend work — still hire heavily for Java, so it remains a strong choice despite the extra ceremony, and Indian services companies in particular recruit for it in volume at entry level.

The mistake that defines Java interviews is comparing strings with ==. That compares references — whether two variables point at the same object in memory — not contents. Two strings with identical characters can be different objects, so == returns false while .equals() returns true. It is asked constantly because it reveals whether you understand that objects are references.

Syntax

public class Payroll {
    public static void main(String[] args) {
        String name = "Ravi";
        int years = 6;
        double salary = 82000.50;
        boolean active = true;

        System.out.println(name + " — " + years + " yrs");
        System.out.printf("Salary: %.2f%n", salary);
    }
}

Key points

  • The file name must match the public class name exactly — Payroll.java for class Payroll.
  • int holds whole numbers, double holds decimals; dividing two ints truncates the result.
  • String is a class (capital S), unlike the primitives int, double and boolean.
The mistake that costs people the interview: Comparing strings with ==, which compares references rather than contents. Use .equals() — this is the classic Java interview trap.

Practice challenge

Boolean ExpressionBasics
Task

Evaluate and print boolean result.

Expected output
true
Show a hint
  1. Use > for comparison
  2. 85 > 80 evaluates to true
  3. Prints "true" or "false"

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. How do you compare two strings' contents in Java?

  1. ==
  2. .equals()
  3. .compare()
  4. ===
Show answer

B. .equals()

2. What does 7 / 2 give when both are int?

  1. 3.5
  2. 3
  3. 4
  4. A compile error
Show answer

B. 3

3. What must match the public class name?

  1. The package
  2. The file name
  3. The method name
  4. Nothing
Show answer

B. The file name

Back to the syllabus ↑

Collections and loops

Working level Java · 18 min · 25 XP

Arrays in Java are fixed in size, which is rarely what you want. ArrayList grows as you add to it, and HashMap stores key–value pairs — between them they cover most everyday Java code, and knowing when to reach for each is most of practical collections work.

The convention interviewers expect is declaring against the interface and instantiating the implementation: List<String> names = new ArrayList<>(). It means the rest of your code depends on what the collection does rather than which one it is, so swapping the implementation later changes one line instead of many.

The runtime error worth knowing before you meet it is ConcurrentModificationException, thrown when you remove from a collection while looping over it. The iterator notices the collection changed underneath it and refuses to continue. The fix is to collect what you want to remove during the loop and remove it afterwards, or use removeIf, which handles the bookkeeping for you.

Syntax

import java.util.*;

List<String> names = new ArrayList<>();
names.add("Priya");
names.add("Ravi");

for (String n : names) {
    System.out.println(n);
}

Map<String, Integer> years = new HashMap<>();
years.put("Priya", 6);
System.out.println(years.getOrDefault("Amit", 0));  // 0

Key points

  • Declare against the interface (List, Map) and instantiate the implementation (ArrayList, HashMap).
  • getOrDefault avoids a null when the key is absent — the Java equivalent of Python's .get().
  • Generics <String> let the compiler reject the wrong type before the program runs.
The mistake that costs people the interview: Modifying a collection while looping over it, which throws ConcurrentModificationException. Collect what to remove, then remove it afterwards.

Practice challenge

2D ArrayWorking level
Task

Create and access 2D array.

Expected output
92
Show a hint
  1. 2D array: int[][] name
  2. Access: array[row][col]
  3. [0][0] is top-left element

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. Which grows as you add items?

  1. String[]
  2. ArrayList
  3. int[]
  4. char[]
Show answer

B. ArrayList

2. What does years.getOrDefault("Amit", 0) return when absent?

  1. null
  2. 0
  3. An exception
  4. -1
Show answer

B. 0

3. What happens if you remove from a list while looping it?

  1. It works fine
  2. ConcurrentModificationException
  3. It skips silently
  4. It reverses order
Show answer

B. ConcurrentModificationException

Back to the syllabus ↑

Conditions, loops and methods

Basics Java · 16 min · 15 XP

Java's if, for and while work as they do elsewhere, but everything must live inside a method, and every method declares what type it returns — void when it returns nothing. That extra structure is the point: the compiler refuses to build code whose types do not line up, so a large class of mistakes never reaches production.

The enhanced for loop, for (int s : scores), avoids the index arithmetic that causes off-by-one errors. Reach for the indexed form only when you genuinely need the position, which is less often than beginners assume.

The rule that catches people writing their first methods is that every path must return. If an if returns a value but the else path falls through with nothing, the code will not compile — Java will not let you write a method that sometimes returns nothing when it promised a String. That strictness feels obstructive for a week and then quietly prevents a category of bug you will never have to debug.

Syntax

public class Grades {
    static String grade(int score) {
        if (score >= 80) return "A";
        else if (score >= 60) return "B";
        return "C";
    }

    public static void main(String[] args) {
        int[] scores = {82, 45, 71};
        for (int s : scores) {
            System.out.println(s + " -> " + grade(s));
        }
    }
}

Key points

  • A method signature declares its return type: String grade(int score).
  • void means the method returns nothing — main is void.
  • The enhanced for (int s : scores) avoids index mistakes entirely.
The mistake that costs people the interview: Forgetting that every path must return. If an if returns but the else path falls through with no return, the code will not compile.

Practice challenge

Hello JavaBasics
Task

Print "Welcome to Java!" using System.out.println()

Expected output
Welcome to Java!
Show a hint
  1. Use System.out.println()
  2. Put message in quotes
  3. Java requires exact structure shown

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What does void mean?

  1. Returns nothing
  2. Returns null
  3. Private
  4. Static
Show answer

A. Returns nothing

2. Which loop avoids index errors?

  1. for (int i=0;...)
  2. for (int s : scores)
  3. while
  4. do-while
Show answer

B. for (int s : scores)

3. What happens if a non-void method has a path with no return?

  1. Returns null
  2. It will not compile
  3. Returns 0
  4. A runtime error
Show answer

B. It will not compile

Back to the syllabus ↑

Objects, null and exceptions

Advanced Java · 20 min · 30 XP

Java is built around objects: a class defines the shape, and each instance holds its own values. Encapsulation — keeping fields private and exposing methods — is the convention interviewers expect you to follow, because it means the object controls its own state rather than trusting every caller to modify it correctly.

The failure that dominates real Java is NullPointerException. A reference that points at nothing throws the moment you use it, and defending against that is a daily habit rather than an edge case. Checking for null, returning empty collections rather than null, and using Optional where a value genuinely may be absent are all part of the same discipline.

The strongest habit is validating in the constructor. If an object cannot be created in an invalid state, every method afterwards can assume its own data is sound, and you stop writing defensive checks throughout the class. Throwing IllegalArgumentException for a blank owner name is not pedantry — it means the invalid object never exists to cause a confusing failure three layers away.

Syntax

public class Account {
    private final String owner;
    private double balance;

    public Account(String owner, double opening) {
        if (owner == null || owner.isBlank())
            throw new IllegalArgumentException("owner required");
        this.owner = owner;
        this.balance = opening;
    }

    public void withdraw(double amt) {
        if (amt > balance) throw new IllegalStateException("Insufficient funds");
        balance -= amt;
    }

    public double getBalance() { return balance; }
}

Key points

  • private fields with public methods is encapsulation — the caller cannot corrupt internal state.
  • final means the reference cannot be reassigned after construction.
  • Validate in the constructor so an invalid object can never exist in the first place.
The mistake that costs people the interview: Calling a method on a possibly-null reference. Check for null, or use Optional, rather than discovering it as a NullPointerException in production.

Practice challenge

Class with ConstructorAdvanced
Task

Define constructor to initialize object.

Expected output
Priya
Show a hint
  1. Constructor name matches class name
  2. Called with new keyword
  3. Initializes object state

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What does private on a field achieve?

  1. Speed
  2. Encapsulation — outside code cannot touch it directly
  3. Thread safety
  4. Serialisation
Show answer

B. Encapsulation — outside code cannot touch it directly

2. What is the most common Java runtime failure?

  1. StackOverflowError
  2. NullPointerException
  3. OutOfMemoryError
  4. ClassCastException
Show answer

B. NullPointerException

3. Why validate inside the constructor?

  1. It is faster
  2. So an invalid object can never exist
  3. It is required by Java
  4. To enable inheritance
Show answer

B. So an invalid object can never exist

Back to the syllabus ↑

Strings, immutability and the loop that melts

Working level Java · 15 min · 15 XP

A Java String cannot be changed after it is created. Every operation that looks like modification — concat, replace, trim, toUpperCase — leaves the original untouched and hands back a new object. Beginners write s.trim(); on its own line, see nothing happen, and conclude the method is broken; it worked perfectly and they discarded the result. Assign it back, or the call was a no-op.

Immutability is a deliberate design decision, not an oversight. It makes strings safe to share between threads, safe to use as map keys, and lets the JVM pool identical literals so the same text is stored once. The cost surfaces in loops: building a string by repeated concatenation allocates a fresh object and copies everything on every iteration, so ten thousand appends do work proportional to the square of the length. StringBuilder exists precisely for this, mutating one buffer instead, and the difference on a large loop is seconds against milliseconds.

The other trap is comparison. == on two Strings asks whether they are the same object in memory, not whether they contain the same characters. It appears to work for literals, because the compiler pools them and both names point at one pooled object — and then fails the moment a string arrives from user input, a file or a database, which is to say in production and not in your test. equals() compares content and is what you almost always want; equalsIgnoreCase() for case-insensitive checks, and Objects.equals() when either side might be null.

Syntax

String s = "  Priya Sharma  ";
s.trim();                       // result DISCARDED -- s is unchanged
s = s.trim();                   // correct

// == compares IDENTITY, equals() compares CONTENT
String a = "hello";
String b = "hello";
System.out.println(a == b);              // true  -- both point at the pooled literal

String c = new String("hello");
System.out.println(a == c);              // false -- different object, same text
System.out.println(a.equals(c));         // true  <- what you meant

Scanner in = new Scanner(System.in);
String typed = in.nextLine();            // "hello", but NOT pooled
if (typed == "hello") { }                // false, always. The classic bug.
if (typed.equals("hello")) { }           // correct

// null-safe either way round
Objects.equals(typed, "hello");

// BUILDING STRINGS IN A LOOP
String out = "";
for (int i = 0; i < 10000; i++) out += i + ",";   // new object EVERY time

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) sb.append(i).append(',');
String result = sb.toString();                     // one buffer, one result

String.format("%s owes %.2f", name, amount);
String.join(", ", List.of("a", "b", "c"));         // a, b, c

Key points

  • Strings are immutable, so every method returns a new one. A call whose result you do not assign has done nothing.
  • == compares object identity. It works by accident on pooled literals and fails on any string that came from input, a file or a database. Use equals().
  • Concatenating in a loop reallocates and copies each time. StringBuilder mutates one buffer and turns a quadratic loop into a linear one.
The mistake that costs people the interview: Using == on strings and having it pass every test. Test data is written as literals, which the compiler pools into the same object, so identity comparison succeeds. Real input is never pooled, so the check silently starts returning false in production against text that looks identical on screen.

Practice challenge

Array SumWorking level
Task

Loop through array and calculate sum.

Expected output
432
Show a hint
  1. Use marks.length for array size
  2. Access array elements with marks[i]
  3. Add to sum in each iteration

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What does s.trim(); on its own line do?

  1. Trims s in place
  2. Nothing to s — the returned new string was discarded
  3. Throws an exception
  4. Trims only trailing spaces
Show answer

B. Nothing to s — the returned new string was discarded

2. Why does == sometimes appear to work on strings?

  1. It compares content for short strings
  2. Identical literals are pooled, so both names reference one object
  3. It is undefined behaviour
  4. Java optimises it into equals()
Show answer

B. Identical literals are pooled, so both names reference one object

3. Why is += in a 10,000-iteration loop slow?

  1. The loop itself is slow
  2. Each concatenation allocates a new string and copies everything, so cost grows quadratically
  3. It runs out of heap
  4. String methods are synchronised
Show answer

B. Each concatenation allocates a new string and copies everything, so cost grows quadratically

Back to the syllabus ↑

equals, hashCode and the object that vanishes

Working level Java · 16 min · 15 XP

Every Java object inherits equals() and hashCode() from Object, and the inherited versions are almost never what you want: equals() compares memory identity, and hashCode() is derived from it. So two Customer objects with identical id, name and email are unequal by default. That is why a list containing a customer reports it is not there, and why removing one silently does nothing — contains() and remove() both call equals().

When you override equals() you must override hashCode() too, and the rule that binds them is one-directional: equal objects must produce equal hash codes, though equal hash codes do not imply equality. Break it and hash-based collections come apart. A HashMap uses hashCode() to pick a bucket and equals() only within that bucket, so two objects that are equal with different hashes land in different buckets — the map holds your entry, get() returns null, and iterating the map shows it plainly sitting there. It looks like a JVM bug and is entirely ordinary.

Two further rules matter in practice. The fields you use must be the ones that define identity — usually the id, not every field, and never a mutable field you will change later, because mutating a key after insertion changes its hash and strands the entry in the wrong bucket. And modern Java gives you the whole thing free: a record generates a correct equals(), hashCode() and toString() from its components, which is why records are the right default for data carriers and why hand-writing these methods should be rare.

Syntax

class Customer {
    final long id; final String name;
    Customer(long id, String name) { this.id = id; this.name = name; }
}

Customer a = new Customer(7, "Priya");
Customer b = new Customer(7, "Priya");
a.equals(b);                    // false -- inherited equals compares IDENTITY
List.of(a).contains(b);         // false

// Override BOTH, always together
@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Customer)) return false;
    return id == ((Customer) o).id;          // id defines identity
}
@Override public int hashCode() { return Long.hashCode(id); }

// BREAK THE CONTRACT AND THE MAP LOSES THINGS
// equals() overridden, hashCode() NOT:
Map<Customer, String> m = new HashMap<>();
m.put(a, "gold");
m.get(b);        // null -- b hashes to a different bucket
m.size();        // 1    -- the entry is right there
m.containsKey(b);// false
// Looks like a JVM bug. It is the contract being broken.

// MUTATING A KEY AFTER INSERTION STRANDS IT
// (which is why identity fields should be final)

// Modern Java: a record generates all three, correctly
record CustomerRec(long id, String name) { }
new CustomerRec(7, "Priya").equals(new CustomerRec(7, "Priya"));  // true

Key points

  • Override equals() and hashCode() together or not at all. Equal objects must have equal hash codes; the reverse is not required.
  • A HashMap picks the bucket by hashCode() and compares by equals() only inside it, so a broken contract makes get() return null for a key the map visibly contains.
  • Base identity on stable fields, ideally final. Mutating a field used in hashCode after insertion strands the entry in a bucket nothing will look in.
The mistake that costs people the interview: Letting the IDE generate equals() and hashCode() over every field, including mutable ones. It compiles and passes a quick test, then an object is modified after being placed in a set, its hash changes, and it becomes unreachable while still being iterable — the most confusing shape a Java bug can take.

Practice challenge

ArrayList BasicsWorking level
Task

Create ArrayList and add/retrieve elements.

Expected output
Raj
Show a hint
  1. ArrayList is like dynamic array
  2. add() appends element
  3. get(index) retrieves element

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. You override equals() but not hashCode(). What happens with HashMap?

  1. Nothing, it works
  2. get() can return null for a key the map contains, because the bucket is chosen by hash
  3. It throws at compile time
  4. The map refuses the entry
Show answer

B. get() can return null for a key the map contains, because the bucket is chosen by hash

2. Which statement is required by the contract?

  1. Equal hash codes imply equal objects
  2. Equal objects must have equal hash codes
  3. Hash codes must be unique
  4. hashCode must be positive
Show answer

B. Equal objects must have equal hash codes

3. What does a record give you?

  1. Only a constructor
  2. A generated equals(), hashCode() and toString() from its components
  3. Automatic persistence
  4. Thread safety
Show answer

B. A generated equals(), hashCode() and toString() from its components

Back to the syllabus ↑

Interfaces, and composition over inheritance

Advanced Java · 17 min · 20 XP

An interface is a contract: it names what an object can do without saying how. Code written against the interface works with any implementation, which is what makes a system testable — a service depending on a PaymentGateway interface can be handed a real one in production and a fake in a test, with no change to the service. That single property is the reason interfaces dominate professional Java, and it is worth stating in an interview because it explains the design rather than describing it.

Inheritance also lets you share behaviour, and it is over-used. Extending a class binds you to its entire implementation forever, in a single-inheritance language where you get one parent and no second chance. The classic demonstration is a Square extending a Rectangle: mathematically sound, and broken in code, because anything setting width and height independently on what it believes is a Rectangle gets nonsense from a Square. The test is whether the subclass genuinely is substitutable for its parent everywhere the parent is used — not merely whether it is one in English.

Composition asks instead what an object has, and it is the better default. A Car holding an Engine can swap it, mock it, or hold two; a Car extending Engine can do none of that. The practical rule that follows: extend a class only when you are genuinely specialising its behaviour and can honour its contract completely; otherwise hold an instance and expose what you need. Since Java 8 interfaces may carry default methods, which lets a contract grow without breaking every implementation — useful, and not a licence to put real logic there.

Syntax

// The contract: what, not how
interface PaymentGateway {
    Receipt charge(long amountPaise, String token);
    default boolean supportsRefund() { return true; }   // Java 8+: grow safely
}

// Production and test implementations are interchangeable
class RazorpayGateway implements PaymentGateway { /* real HTTP */ }
class FakeGateway implements PaymentGateway {       // no network in tests
    Receipt charge(long a, String t) { return new Receipt("test", a); }
}

class CheckoutService {
    private final PaymentGateway gateway;            // depends on the CONTRACT
    CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
}
new CheckoutService(new FakeGateway());   // testable without a network

// INHERITANCE THAT LOOKS RIGHT AND IS WRONG
class Rectangle { void setWidth(int w){} void setHeight(int h){} }
class Square extends Rectangle { /* must keep sides equal */ }

void resize(Rectangle r) { r.setWidth(5); r.setHeight(4); }
// area 20 for a Rectangle; a Square cannot honour both. Compiles, wrong.
// The test is substitutability, not whether "a square is a rectangle".

// COMPOSITION: has-a, not is-a
class Car {
    private final Engine engine;          // swappable, mockable, replaceable
    Car(Engine engine) { this.engine = engine; }
    void start() { engine.ignite(); }
}
// Extend only to specialise behaviour you can fully honour. Otherwise hold one.

Key points

  • Depending on an interface rather than a class is what makes code testable: the same service takes a real implementation in production and a fake in a test.
  • Inheritance is correct only when the subclass is substitutable for the parent everywhere. 'Is-a' in English is not the test; honouring the contract is.
  • Composition — holding an instance — keeps the option to swap, mock or hold several. Java gives you one parent, so spend it carefully.
The mistake that costs people the interview: Extending a class to reuse a couple of its methods. You inherit its entire surface, its constructor requirements and its future changes, in a language where that is your only parent — and the coupling shows up later as a change in the base class breaking subclasses nobody remembered existed.

Practice challenge

Collections FrameworkAdvanced
Task

Create HashMap and retrieve values.

Expected output
92
Show a hint
  1. HashMap stores key-value pairs
  2. put(key, value) adds entry
  3. get(key) retrieves value

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. Why does depending on an interface make code testable?

  1. Interfaces are faster
  2. Any implementation satisfies it, so a fake can replace the real one with no change to the caller
  3. Interfaces cannot fail
  4. It removes the constructor
Show answer

B. Any implementation satisfies it, so a fake can replace the real one with no change to the caller

2. What is the real test for using inheritance?

  1. Whether it reads as 'is-a' in English
  2. Whether the subclass is substitutable for the parent everywhere the parent is used
  3. Whether it saves lines
  4. Whether the parent is abstract
Show answer

B. Whether the subclass is substitutable for the parent everywhere the parent is used

3. What does composition preserve that inheritance does not?

  1. Compile speed
  2. The ability to swap, mock or hold several of the collaborator
  3. Access to private fields
  4. Static typing
Show answer

B. The ability to swap, mock or hold several of the collaborator

Back to the syllabus ↑

Streams, lambdas and Optional

Advanced Java · 18 min · 20 XP

A stream expresses what you want done to a collection rather than how to iterate it. Filter, map and collect replace the loop-with-an-accumulator that dominates older Java, and the gain is readability more than speed: the shape of the transformation is visible in one expression instead of reconstructed from a loop body. Streams are also lazy — nothing runs until a terminal operation such as collect, forEach or count asks for a result, which is why a pipeline with no terminal operation executes nothing at all and looks like dead code.

Two properties catch people out. A stream is single-use: consume it and any further operation throws IllegalStateException, so a stream stored in a variable and used twice fails at runtime rather than compile time. And streams do not mutate the source; they produce a new result, so a pipeline whose value is discarded has achieved nothing, exactly like an unassigned string operation. The collectors worth knowing early are toList, joining, groupingBy and counting, because they cover most of what a loop was doing.

Optional addresses the other half: methods that may have no answer. Returning Optional<Customer> instead of a nullable Customer moves the absent case into the type, so a caller cannot forget it — the compiler makes them handle it. Used properly that means orElse, orElseGet, map or ifPresent; used improperly it means calling isPresent() then get(), which is the null check written more verbosely and gains nothing. And Optional is for return types: as a field or a parameter it adds ceremony without adding safety.

Syntax

record Order(String customer, String region, long amount, String status) {}

// WHAT, not how
List<String> bigCustomers = orders.stream()
    .filter(o -> o.amount() > 100_000)
    .map(Order::customer)
    .distinct()
    .sorted()
    .toList();

// grouping replaces the loop-with-a-map
Map<String, Long> revenueByRegion = orders.stream()
    .collect(Collectors.groupingBy(Order::region,
             Collectors.summingLong(Order::amount)));

Map<String, Long> countByStatus = orders.stream()
    .collect(Collectors.groupingBy(Order::status, Collectors.counting()));

String names = orders.stream().map(Order::customer)
    .collect(Collectors.joining(", "));

// LAZY: no terminal operation, nothing runs
orders.stream().filter(o -> o.amount() > 0);      // does nothing at all

// SINGLE-USE: consuming twice throws at RUNTIME
var s = orders.stream();
s.count();
s.count();                                        // IllegalStateException

// OPTIONAL: absence in the type system
Optional<Order> findById(String id) { ... }

String who = findById("A-1").map(Order::customer).orElse("unknown");
findById("A-1").ifPresent(o -> log(o.customer()));

// pointless -- this is a null check with extra words
if (findById("A-1").isPresent()) { var o = findById("A-1").get(); }

// Optional is for RETURN types. As a field or parameter it is ceremony.

Key points

  • Streams are lazy: without a terminal operation such as collect, count or forEach, nothing executes and the pipeline is dead code.
  • A stream is single-use and does not mutate its source. Reusing one throws at runtime, and discarding the result achieves nothing.
  • Optional belongs on return types, where it forces the caller to handle absence. isPresent() followed by get() is a null check written longer.
The mistake that costs people the interview: Calling .get() on an Optional because the value 'is always there'. That is the null pointer exception you were avoiding, now thrown as NoSuchElementException — and the compiler had offered you orElse, orElseThrow with a real message, or map, any of which would have documented what should happen when it is not.

Practice challenge

Find MaximumAdvanced
Task

Create method to find max value in array.

Expected output
95
Show a hint
  1. Start max with first element
  2. Compare each with current max
  3. Update max when larger found

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. A stream pipeline with filter and map but no collect. What runs?

  1. Everything
  2. Nothing — streams are lazy until a terminal operation
  3. Only filter
  4. It throws
Show answer

B. Nothing — streams are lazy until a terminal operation

2. You store a stream and call count() twice. What happens?

  1. Both work
  2. The second throws IllegalStateException — a stream is single-use
  3. It returns 0
  4. It recomputes
Show answer

B. The second throws IllegalStateException — a stream is single-use

3. Where does Optional belong?

  1. On fields
  2. On return types, so callers must handle absence
  3. On parameters
  4. Everywhere nullable
Show answer

B. On return types, so callers must handle absence

Back to the syllabus ↑

Files, resources and the leak you cannot see

Advanced Java · 17 min · 20 XP

Anything you open — a file, a socket, a database connection — must be closed, and the operating system gives each process a limited number of handles. A program that leaks them runs correctly for hours and then fails with 'too many open files' under load, at which point the stack trace points at whatever unlucky call needed the next handle rather than at the code that leaked. That distance between cause and symptom is what makes resource leaks disproportionately expensive to debug.

The old solution was a finally block, and it was easy to get wrong: close() itself can throw, nesting two resources doubles the ceremony, and an exception in close can mask the original exception so you see the wrong error. try-with-resources replaces all of it. Any object implementing AutoCloseable declared in the try header is closed automatically, in reverse order, whether the block completes or throws — and if both the body and close throw, the close exception is attached as suppressed rather than replacing the real one.

For the reading itself, prefer the java.nio.file API over the older File class: Files.readAllLines for a small file, Files.lines for a large one you want to stream lazily, Files.readString when you want the whole thing as text. The distinction matters at scale, because readAllLines on a two-gigabyte file loads two gigabytes into heap while Files.lines processes it a line at a time — but note that Files.lines returns a stream backed by an open file, so it too must sit in a try-with-resources or you have leaked the handle in the very act of being careful about memory.

Syntax

// try-with-resources: closed automatically, in reverse order,
// on success or on exception
try (BufferedReader r = Files.newBufferedReader(Path.of("orders.csv"));
     BufferedWriter w = Files.newBufferedWriter(Path.of("clean.csv"))) {
    String line;
    while ((line = r.readLine()) != null) {
        if (!line.isBlank()) w.write(line + System.lineSeparator());
    }
}   // both closed here, w first

// small file, whole thing in memory
List<String> lines = Files.readAllLines(Path.of("small.csv"));
String text = Files.readString(Path.of("note.txt"));

// LARGE file: stream lazily -- but the stream holds the file open,
// so it needs try-with-resources too
try (Stream<String> lines2 = Files.lines(Path.of("huge.csv"))) {
    long errors = lines2.filter(l -> l.contains("ERROR")).count();
}

// THE LEAK: no close at all
BufferedReader r = Files.newBufferedReader(path);
r.readLine();
// handle never released. Works fine... until "too many open files",
// thrown by whatever unlucky code needed the NEXT handle.

// Files.exists then read is a race; just handle the exception
try {
    return Files.readString(path);
} catch (NoSuchFileException e) {
    return "";
}

// Anything you write can implement AutoCloseable and join the pattern
class Report implements AutoCloseable {
    @Override public void close() { /* release */ }
}

Key points

  • try-with-resources closes every declared AutoCloseable in reverse order, on success or on exception, and keeps the original exception with close's attached as suppressed.
  • Files.lines streams a large file lazily instead of loading it, but the stream holds the file open — it needs try-with-resources as much as a reader does.
  • A leaked handle fails far from its cause. The exception surfaces in whatever code needed the next handle, which is why the stack trace points at the wrong place.
The mistake that costs people the interview: Checking Files.exists(path) before reading and treating that as safety. The file can vanish between the check and the read, the check costs an extra system call, and you still need the exception handler — so the check adds a race and removes nothing.

Practice challenge

Generics BasicAdvanced
Task

Define generic method with type parameter.

Expected output
1 2 3
Show a hint
  1. <T> defines type parameter
  2. T is placeholder for any type
  3. Called with different types each time

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What does try-with-resources guarantee?

  1. The block cannot throw
  2. Every declared AutoCloseable is closed, in reverse order, on success or failure
  3. Resources are pooled
  4. Exceptions are swallowed
Show answer

B. Every declared AutoCloseable is closed, in reverse order, on success or failure

2. Why must Files.lines sit in a try-with-resources?

  1. It is slow otherwise
  2. The returned stream holds the file open and must be closed
  3. It buffers the whole file
  4. It is not thread safe
Show answer

B. The returned stream holds the file open and must be closed

3. Where does a 'too many open files' error usually surface?

  1. At the leaking code
  2. At whatever unrelated call needed the next handle
  3. At compile time
  4. At JVM startup
Show answer

B. At whatever unrelated call needed the next handle

Back to the syllabus ↑

Tests, builds and shipping something real

Job-ready Java · 18 min · 25 XP

The gap between people who can write Java and people who get hired is usually the surrounding machinery, and it is smaller than it looks. A build tool — Maven or Gradle — declares your dependencies and your Java version in one file, downloads what it needs, compiles, runs tests and produces a jar. Its real function is reproducibility: anyone who clones the project runs one command and gets an identical build, which is why 'it works on my machine' stops being a sentence people say.

JUnit is the test framework and the shape is always the same: arrange the inputs, act by calling the thing, assert the outcome. What separates a useful suite from a decorative one is which cases you write. The happy path is the least valuable test because it is the one you already ran by hand. The value is in the empty input, the null, the boundary, the duplicate and the error path — and in one regression test for every bug you fix, which is what stops the same bug returning in six months.

Two habits carry disproportionate weight in an interview. Name tests for the behaviour they pin, so a failure reads as a sentence about what broke rather than 'test3 failed'. And use assertThrows for the failure cases, because a method that is supposed to reject bad input needs a test proving it does — an exception that stops being thrown is a silent behaviour change that no happy-path test will ever notice. Beyond that, keep tests independent and fast: a suite people skip because it takes four minutes is a suite that is not protecting anything.

Syntax

<!-- pom.xml: dependencies and Java version declared once -->
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.10.2</version>
  <scope>test</scope>
</dependency>

// mvn test | mvn package  -> anyone gets an identical build

class InvoiceTest {

    @Test
    void totalIncludesTaxRoundedToPaise() {      // names the BEHAVIOUR
        var invoice = new Invoice(List.of(new Line("A", 10_000L)));  // arrange
        long total = invoice.totalWithTax(0.18);                     // act
        assertEquals(11_800L, total);                                // assert
    }

    @Test
    void emptyInvoiceTotalsZeroRatherThanThrowing() {
        assertEquals(0L, new Invoice(List.of()).totalWithTax(0.18));
    }

    @Test
    void negativeTaxRateIsRejected() {
        var invoice = new Invoice(List.of(new Line("A", 100L)));
        var e = assertThrows(IllegalArgumentException.class,
                             () -> invoice.totalWithTax(-0.1));
        assertTrue(e.getMessage().contains("tax"));
    }
    // ^ a method that must REJECT bad input needs a test proving it does.
    //   An exception that stops being thrown is a silent behaviour change.

    @ParameterizedTest
    @ValueSource(longs = {0, 1, Long.MAX_VALUE})   // boundaries
    void handlesEdgeAmounts(long amount) { ... }
}

// Write a regression test for every bug you fix. That is what stops
// it coming back in six months.

Key points

  • A build file makes the project reproducible: one command, identical result on any machine. That is what ends 'it works on mine'.
  • The happy path is the least valuable test — you already ran it by hand. Empty, null, boundary, duplicate and error cases are where defects live.
  • Use assertThrows for rejection behaviour. Without it, a validation that quietly stops throwing passes every remaining test.
The mistake that costs people the interview: Writing tests only for the path you already know works, so the suite is green and proves nothing. It gives the strongest possible false signal — a passing build — for the code least likely to fail, while the empty list, the null and the boundary go untested straight into production.

Practice challenge

InheritanceAdvanced
Task

Student inherits from Person.

Expected output
Hello, I'm a student
Show a hint
  1. class Child extends Parent
  2. Child inherits all parent methods
  3. Override by redefining method

Open this exercise in the app → · Auto-graded coding rounds

Check yourself

1. What is the main purpose of a build tool like Maven?

  1. Faster compilation
  2. A reproducible build: declared dependencies and version, identical on any machine
  3. Code formatting
  4. Deployment
Show answer

B. A reproducible build: declared dependencies and version, identical on any machine

2. Which test is least valuable?

  1. The empty input case
  2. The happy path you already ran manually
  3. The boundary case
  4. The error path
Show answer

B. The happy path you already ran manually

3. Why use assertThrows?

  1. It is required by JUnit
  2. To prove a method rejects bad input — otherwise a validation that stops throwing goes unnoticed
  3. It speeds up tests
  4. It replaces assertEquals
Show answer

B. To prove a method rejects bad input — otherwise a validation that stops throwing goes unnoticed

Back to the syllabus ↑

Common questions

Do I need any background to start Java?

No. This track begins at its own beginning and assumes nothing, and the first lesson explains what the thing is before showing you any syntax.

How long does the Java track take?

About 170 minutes of reading across 10 lessons, plus the practice challenges and 30 multiple-choice questions, which is where the time actually goes.

Is it free?

Yes, and there is no account. Everything runs in your browser.

More: all 15 tracks · what employers actually ask for · the full syllabus

Keep reading

The STAR method, properly: how to build answers that hold up
A working guide to STAR interview answers: how to weight each part, how to build five stories that cover most…
Returning to work after a career break: rebuilding confidence and explaining the gap
How to present a career break on your CV, close the confidence gap, and answer interview questions about time…
Free AI interview coach
Free AI interview coach: voice mock interviews that talk back, role-specific questions, coding practice and…
Interview countdown, prediction & mock practice
Free interview prep: a live countdown to your interview date, then the 15 most common questions as flip-cards…