Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Language & library reference

Concept clusters →

Every named result in this course, grouped by chapter. Each one links back to where it's derived.

Language fundamentals

Rule

Widening is implicit, narrowing needs a cast

also known as: widening and narrowing primitive conversions, casts

Formal. Java converts a primitive value to another primitive type automatically only along the widening chain byte โ†’ short โ†’ int โ†’ long โ†’ float โ†’ double (and char โ†’ int onward), where every value of the source type fits the target's range. A conversion the other way is narrowing and needs a cast, (int) x. A cast from double to an integer type drops the fraction, truncating toward zero; a cast to a smaller integer type keeps only the low bits, so the value wraps. boolean converts to nothing.

In plain English. Pouring a small cup into a big jug is always safe, so Java does it for you. Pouring a big jug into a small cup might spill, so Java makes you say "I know, do it anyway" by writing a cast, and what spills is simply lost: (int) 9.99 is 9, not 10.

Example. long total = 42; and double ratio = count; compile. int whole = 9.99; fails with possible lossy conversion from double to int; int whole = (int) 9.99; compiles and stores 9. (byte) 300 is 44.

Why this rule exists. Silent data loss is one of the oldest sources of bugs. Making every lossy conversion visible in the source keeps it deliberate.

Rule

Precedence first, then left to right

also known as: operator precedence and associativity

Formal. In a Java expression, *, / and % bind tighter than binary + and -, which bind tighter than the comparisons, which bind tighter than &&, which binds tighter than ||. Operators of the same precedence group left to right, except assignment, which groups right to left. Operands are evaluated left to right, and && and || evaluate their right operand only when the left one doesn't already decide the result.

In plain English. Do multiplication, division and remainder before adding and subtracting, then work from the left, just as in school arithmetic. Brackets override everything, so when in doubt, add them.

Example. 2 + 3 * 4 is 14, 20 - 5 - 3 is 12, and 24 / 4 / 2 is 3. "Total: " + 2 + 3 is "Total: 23" because the left + meets a String first, while 2 + 3 + " items" is "5 items".

Why this rule exists. A fixed, familiar order lets everyone read the same expression the same way, without brackets around every step.

Strings, arrays, and basic data processing

Rule

Strings are immutable

also known as: String immutability

Formal. A String object never changes after it is created. Every method that seems to modify text, such as toUpperCase, strip, replace or substring, returns a new String and leaves the original untouched. A variable can be reassigned to refer to the new string; the old object is unchanged. == compares references, so text must be compared with equals (or equalsIgnoreCase).

In plain English. A string is printed on paper, not written on a whiteboard. Asking for an upper-case version prints a fresh sheet. If you don't keep the fresh sheet, by assigning it to a variable, nothing has changed.

Example. After String name = " ada "; name.strip(); the variable still holds " ada ". name = name.strip(); keeps the result. "ab-cd".replace("-", "") returns "abcd" and replaces every match.

Why this rule exists. Because no one can change a string behind your back, strings are safe to share and to cache, and to use as lookup keys.

Collections and generics

Rule

What the main collection operations cost

also known as: Big-O of ArrayList, LinkedList, HashMap, TreeMap

Formal. With n elements: ArrayList.get(i), set(i, x) and add(x) (amortized) are O(1); add(0, x), remove(0) and contains(x) are O(n). LinkedList.get(i) is O(n); adding or removing at either end is O(1). HashMap.get, put and containsKey, and HashSet.add and contains, are O(1) on average. TreeMap and TreeSet operations are O(log n). PriorityQueue.peek is O(1); offer and poll are O(log n).

In plain English. Jumping to a numbered slot or looking up a key in a hash table takes the same time however big the collection is. Searching a list checks every item, so it grows with the list. A sorted tree takes a few more steps each time the data doubles, about 20 steps for a million items.

Example. Checking a million IDs with list.contains(id) inside a loop over another million IDs is quadratic: about a trillion steps. Putting the IDs in a HashSet first makes each check constant and the whole loop linear.

Why this rule exists. The costs follow from how each class stores its elements: an array for ArrayList, a chain of nodes for LinkedList, buckets chosen by hashCode for HashMap, a balanced tree for TreeMap.

Lambdas, functional programming, and streams

Rule

The four core functional interfaces

also known as: Supplier, Consumer, Function, Predicate

Formal. In java.util.function: Supplier<T> has T get() (nothing in, a value out); Consumer<T> has void accept(T t) (a value in, nothing out); Function<T, R> has R apply(T t) (a value in, a different value out); Predicate<T> has boolean test(T t) (a value in, true or false out). UnaryOperator<T> is a Function<T, T>, and BiFunction<T, U, R> takes two inputs.

In plain English. Name the interface by what goes in and what comes out. Nothing in and something out is a supplier. Something in and nothing out is a consumer. Something in and something else out is a function. Something in and a yes or no out is a predicate.

Example. () -> new ArrayList<String>() is a Supplier, s -> IO.println(s) a Consumer, String::length a Function<String, Integer>, and s -> s.isBlank() a Predicate<String>. filter takes a Predicate, map a Function, forEach a Consumer.

Why this rule exists. Four standard shapes mean library methods and your own code agree on what a lambda receives and returns, without a new interface for every job.

Rule

Streams are lazy and single-use

also known as: intermediate and terminal operations

Formal. A stream pipeline is a source, zero or more intermediate operations (filter, map, sorted, distinct, limit, skip), and one terminal operation (toList, collect, count, forEach, reduce, findFirst). Intermediate operations only record a step and return a new stream; nothing is read from the source until the terminal operation runs. A stream can be consumed once: reusing it throws IllegalStateException. A stream never modifies its source.

In plain English. Writing the steps of a pipeline is like writing a recipe: nothing is cooked until the last line says "serve". And each stream is a single pass over the data: to go again, ask the collection for a new stream.

Example. orders.stream().filter(o -> o.total() > 100) on its own does nothing. Adding .count() runs it. Calling count() twice on the same stream object throws IllegalStateException: stream has already been operated upon or closed.

Why this rule exists. Laziness lets the JVM pass each element through every step before reading the next one, and lets limit and findFirst stop early, even on an endless source.

Exceptions and error handling

Rule

Checked exceptions must be caught or declared

also known as: the catch-or-declare rule, checked and unchecked exceptions

Formal. Every exception is a Throwable. Error and RuntimeException, with all their subclasses, are unchecked. Every other Throwable (for example IOException and InterruptedException) is checked: if a statement can throw a checked exception, its method must either catch it or declare it with a throws clause. The compiler checks this one method at a time, from method headers.

In plain English. Some failures, like a missing file, are a normal part of the world, and Java makes you write down a plan for them: handle it here, or warn your callers in the method's header. Programming mistakes, like a null where an object should be, aren't checked; the fix is to correct the code.

Example. Files.readAllLines(path) is declared throws IOException, so calling it without a try/catch or a throws IOException on the enclosing method fails to compile with unreported exception IOException; must be caught or declared to be thrown. Integer.parseInt("x") throws the unchecked NumberFormatException, and needs neither.

Why this rule exists. It makes the recoverable failures a method can cause part of its signature, so callers can't forget them.

Maven, Gradle, dependencies, and project structure

Rule

A Maven phase runs every phase before it

also known as: the Maven default lifecycle

Formal. Maven's default lifecycle is an ordered list of phases; the ones typed most are validate, compile, test-compile, test, package, verify, install and deploy. mvn <phase> runs every phase of that lifecycle up to and including the named one, in order, and stops at the first failure. clean belongs to a separate lifecycle, so mvn clean package runs clean, then the default lifecycle up to package. Phases do nothing by themselves: plugin goals bound to them do the work.

In plain English. The phases are a checklist that always runs from the top. Asking for package means "do everything up to and including making the JAR", so the tests always run first, and a failing test stops the build before any JAR is made.

Example. mvn package compiles, runs the unit tests, then builds the JAR in target/. mvn install does all that, then verify, then copies the JAR into ~/.m2/repository.

Why this rule exists. A fixed order means every developer and every build server produces the same artifact the same way, from one short command.

Networking, HTTP, and REST APIs

Rule

The first digit of an HTTP status is its class

also known as: HTTP status code classes

Formal. An HTTP status code's first digit is its class: 1xx informational, 2xx success, 3xx redirection, 4xx client error, 5xx server error. A client that doesn't know a code treats it as the x00 code of its class. A 4xx won't succeed if resent unchanged; a 5xx, 408 or 429 may succeed later.

In plain English. Read the first digit first. 2 means it worked, 3 means look somewhere else, 4 means your request was wrong, and 5 means the server went wrong. The first digit tells you whose problem it is and what to do next.

Example. 200 OK, 201 Created, 204 No Content; 301 Moved Permanently, 304 Not Modified; 400 Bad Request, 401 Unauthorized (no valid credentials), 403 Forbidden (known, but not allowed), 404 Not Found, 409 Conflict; 500 Internal Server Error, 503 Service Unavailable.

Why this rule exists. New codes can be added without breaking old clients, because every client can still act on the class.

The JVM, memory, and performance

Rule

Only unreachable objects are collected

also known as: tracing garbage collection, GC roots

Formal. The garbage collector starts from the GC roots (references in the local variables of running frames, static fields of loaded classes, and references the JVM holds), follows every reference it can reach, and marks what it finds. Every object it never reaches is unreachable, and its memory may be reused. Cycles of objects with no path from a root are collected. An object that is still reachable is never collected, whether or not the program will use it again.

In plain English. Java keeps everything you can still get to by following references from your running code, and throws away the rest. A "memory leak" in Java is memory you forgot you were still holding, such as a list that only ever grows.

Example. After order = null;, if nothing else refers to the old Order, it becomes garbage. Two objects that refer only to each other are garbage too. A static List<Order> that is added to on every request keeps every order alive forever.

Why this rule exists. Reachability is something the JVM can check exactly, so automatic memory management never frees an object your code could still use.

Packages, modules, and application distribution

Rule

A module reads what it requires and shows what it exports

also known as: module-info.java directives

Formal. A module is declared in module-info.java. requires m; makes it read module m; requires transitive m; also grants that read to every module that requires this one. exports p; makes the public types of package p usable by other modules at compile time and run time; exports p to m; limits that to the named modules. opens p; additionally allows deep reflection into p at run time. A package that isn't exported stays inside the module even when its types are public. Exports name single packages, never subpackages.

In plain English. A module is a walled garden. requires lists the gardens you visit, exports puts a door in your wall for one package, and opens lets frameworks look inside that package with reflection. public alone no longer means "everyone can see it".

Example. module com.shop.app { requires com.shop.model; } can use com.shop.model.Order only if com.shop.model has exports com.shop.model;. Its package com.shop.model.internal stays hidden.

Why this rule exists. Strong encapsulation lets a library keep its internals private, so it can change them without breaking anyone.

Java design and architecture

Rule

The SOLID principles

also known as: SRP, OCP, LSP, ISP, DIP

Formal. Single responsibility: a class should have one reason to change. Open-closed: code should be open for extension and closed for modification, so a new kind of thing is added without editing working code. Liskov substitution: a subtype must work anywhere its supertype is expected, keeping its promises. Interface segregation: no client should be forced to depend on methods it doesn't use. Dependency inversion: high-level policy and low-level details should both depend on abstractions, with the interface owned by the high-level side.

In plain English. Five checks for a design: one job per class; add, don't edit; a subclass must really be usable as its parent; small interfaces; and the important code shouldn't know which database or web library it runs on.

Example. A ReportService that formats, emails and stores reports has three reasons to change (SRP). A switch over payment types that grows with every new type breaks OCP; a PaymentMethod interface with one class per type doesn't.

Why this rule exists. Each principle limits how far a change has to spread, which is what makes a code base cheap to keep changing.