Posts

Files, I/O & NIO: Reading and Writing Data

Almost every program you've written so far has worked with data that vanishes when the program ends. Real programs keep data around: they read configuration files, load CSV exports, write logs, generate reports. Java has two file APIs — the 1996 original ( java.io ) and the modern java.nio.file package, introduced in Java 7 and known as NIO.2 . In 2026, NIO.2 is the default: it is shorter, safer, and harder to misuse. You'll still meet java.io in legacy code and libraries, so this post teaches NIO.2 first and gives you just enough java.io to read old code without fear. This is the final post of the Core Java track — it leans on exceptions and try-with-resources (post 10), collections (post 11), and streams (post 15). The next track, Tooling & Testing, assumes you can read and write files comfortably. The decision rule: which API do I reach for? Writing new code? Use java.nio.file ( Path + Files ). Reading old code or a library's streams? Learn the java.io sh...

Date, Time & Money: java.time and BigDecimal Done Right

Two of the most common sources of production bugs in Java applications are date-time handling and money arithmetic. The old java.util.Date and Calendar classes were mutable (so one method could silently change a date it didn't own), had zero-based months (January is 0 , which has confused every Java developer at least once), and mixed up "a moment in time" with "a wall-clock reading" in ways that made timezone bugs nearly inevitable. We mention them once so you can read legacy code — from here on, everything is java.time (Java 8+, current best practice in Java 25) and BigDecimal . The java.time Toolbox: Pick the Right Type java.time gives you several distinct types because dates and times are genuinely different concepts. Picking the right one is a decision rule, not a vocabulary test: LocalDate , LocalTime , LocalDateTime — a calendar date and/or a wall-clock time with no timezone attached . Use them for things that are inherently local: a store's...

Records, Sealed Classes & Pattern Matching (Java 17+)

The sixty-line Point Open any older Java codebase and you will find classes like this: two fields, a constructor, two getters, and then equals , hashCode , and toString — written by hand or by an IDE — for a total of fifty or sixty lines whose only job is to carry two integers. public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } // ... then equals(): 20 lines comparing x and y ... // ... then hashCode(): consistent with equals ... // ... then toString(): "Point{x=3, y=4}" ... } Every line is correct and every line is noise. None of it says anything about your problem; it says "this is a value with two parts," repeated in the syntax Java used to demand. Records delete that ceremony. Sealed classes solve the mirror problem — hierarchies that are supposed to be closed but aren't. And patte...

Streams, Collectors & Optional

Streams, Collectors & Optional Post 14 gave you lambdas — small functions you can pass around. This post shows where lambdas earn their keep: streams for bulk data processing, and Optional for saying "this value might be missing" without null landmines. The stream pipeline mental model Forget the individual method list for a moment. A stream pipeline always has exactly three parts: Source Intermediate ops (lazy) Terminal (eager) Hold onto one rule that explains almost everything in this post: Intermediate operations are lazy — they describe the work. One terminal operation is eager — it does the work, in a single fused pass. List<String> names = List.of("anita", "bob", "farah", "bo", "kunal"); long count = names.stream() // source .filter(n -> n.length() >= 4) // intermediate (lazy) .map(String::toUpperCase) // intermediate (lazy)...

Lambdas & Functional Interfaces

Lambdas & Functional Interfaces Up to this point in the track, every piece of behavior in your programs has been a method living inside a class. In this post you learn how to treat behavior itself as a value — something you can pass to a method, return from a method, and store in a variable. The machinery behind it is the functional interface plus lambda expressions , and this post sets up the Streams API you'll meet next. The functional interface, in one paragraph A functional interface is any interface with exactly one abstract method . That's the whole definition. The interface may also carry default methods, static methods, and declarations of Object 's methods — none of those break the rule, because only the single abstract method counts. You met functional interfaces in the interfaces post. The point to lock in now: because there is only one method to implement, the compiler can figure out which method you're implementing without you naming it. Th...

Generics & Wildcards: PECS Without Tears

The Bug That Generics Killed Before Java 5, every collection was a bag of Object . The compiler didn't care what you put in, and it was your job to cast everything coming out. It worked — until it didn't: // The pre-generics world (Java 1.4 style). Do not write this. List scores = new ArrayList(); scores.add(95); // int → Integer, no problem scores.add("absent"); // also no problem — the compiler shrugs scores.add(88); int total = 0; for (Object o : scores) { total += (Integer) o; // blind cast: "trust me, compiler" } // Runs for a while… then: ClassCastException: String cannot be cast to Integer // …at 3 a.m., in production. The failure mode was the problem, not the cast. Nothing was wrong at compile time. The program ran fine for weeks until one record with unexpected data hit the loop, and then it exploded. Generics exist to move that failure from runtime to compile time. That's the whole pitch: if the wrong thing goes in, yo...

Collections II: How HashMap Really Works

You've used HashMap since the collections tour: put a key in, get a value out, about O(1). That about is doing a lot of work. A HashMap 's speed isn't a promise of the Map interface — it's the emergent result of five internal decisions: the bucket array, the way it perturbs your hashCode , what happens when two keys collide, when the table resizes, and the load factor that governs it all. You can drive a HashMap for years without knowing any of this. But the day one sits on a hot path — a session cache, a dedup index, a request router — the internals become your debugging vocabulary. This post builds that vocabulary, leaning on the equality-and-hashing rules from the earlier post on equals and hashCode , and on the Map tour from the collections post. 1. A HashMap is an array of buckets Forget "hash table" as a black box. A HashMap is an array — internally a Node<K,V>[] table — where each slot is a bucket holding either nothing or the head of...