☕ Java

Private Interface Methods

Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.

Motivation — The Code-Duplication Gap Left by Java 8's Default Methods

Java 8's default methods allowed interfaces to carry shared, concrete behavior for the first time, but they introduced a narrower follow-on problem: if an interface has two or more default methods that need to share some piece of common logic — a validation check, a formatting routine, a small calculation — Java 8 offered no way to factor that shared logic into a helper that was usable from default methods but not part of the interface's actual public contract. The only two options were duplicating the logic verbatim inside each default method that needed it (creating a maintenance burden where a bug fix or behavior change had to be applied in multiple places), or extracting it into another default method, which technically solved the duplication but came at the cost of exposing what was conceptually an internal implementation detail as a public-facing method that any implementing class or external caller could now also see, call, and even override — polluting the interface's API surface with something that was never meant to be part of its contract. Java 9 closed this gap by allowing private methods inside interfaces: a method declared private within an interface, complete with a method body, that is visible and callable only from other methods declared in that same interface — its default methods, its other private methods, and (for private static methods specifically) its static methods — and entirely inaccessible to implementing classes or any code outside the interface. This gives interface authors exactly the same kind of "factor out a shared helper, but keep it an implementation detail" capability that private methods have always given to classes, applied for the first time to interfaces.
Java
// ── Java 8 problem: shared logic between default methods, no clean way to factor it out
interface ValidationRulesV8 {
    default boolean isValidEmail(String email) {
        // duplicated null/blank check:
        if (email == null || email.isBlank()) return false;
        return email.contains("@");
    }

    default boolean isValidUsername(String username) {
        // SAME null/blank check, duplicated again:
        if (username == null || username.isBlank()) return false;
        return username.length() >= 3;
    }
    // Any fix to the null/blank logic must be applied in BOTH places —
    // or this logic gets extracted as a public default method, exposing
    // what's really just an internal helper as part of the public API:
    default boolean isBlankOrNull(String s) {   // now callable by ANYONE — API pollution
        return s == null || s.isBlank();
    }
}

// ── Java 9+ solution: private interface method — shared, but truly hidden
interface ValidationRulesV9 {
    default boolean isValidEmail(String email) {
        return !isBlankOrNull(email) && email.contains("@");
    }

    default boolean isValidUsername(String username) {
        return !isBlankOrNull(username) && username.length() >= 3;
    }

    // Private — usable by the default methods above, invisible to everyone else:
    private boolean isBlankOrNull(String s) {
        return s == null || s.isBlank();
    }
}

class UserValidator implements ValidationRulesV9 {}
UserValidator v = new UserValidator();
System.out.println(v.isValidEmail("a@b.com"));   // true
// v.isBlankOrNull("x");   // COMPILE ERROR — private to the interface, not visible at all

Private Instance Methods vs Private Static Methods in Interfaces

Java 9 actually introduced two distinct kinds of private interface methods, mirroring the existing instance/static distinction for default and static interface methods. A plain private method (no static modifier) behaves like a private default method in spirit: it can be called from the interface's default methods and from its other private instance methods, and — because it is an instance-context method — it is allowed to call the interface's other abstract methods, since at the point it's actually invoked there will always be a concrete implementing instance backing those abstract method calls. A private static method, by contrast, has no instance context at all (no this), and can be called from anywhere within the interface that also has no instance context — the interface's own static methods, and additionally from default methods and private instance methods (which do have an instance context, but can still freely call static methods, exactly as static and instance methods can call each other within an ordinary class). A private static method cannot call the interface's abstract or default instance methods directly without an explicit instance reference, for the same reason a static method in a class cannot call an instance method without an explicit object reference — there is no implicit this available in a static context. In both cases, the core visibility guarantee is identical: private interface methods, instance or static, are never inherited by implementing classes, never visible outside the interface's own body, and cannot be invoked via the interface name or any instance reference from outside code — they exist purely to let the interface's own default and static methods share logic internally.
Java
interface Calculator {
    // Public abstract method — the actual contract:
    double baseValue();

    // Default (instance) method — public, calls a private instance helper:
    default double withTax(double taxRate) {
        return applyRate(baseValue(), taxRate);   // calling abstract method is fine — instance context
    }

    default double withDiscount(double discountRate) {
        return applyRate(baseValue(), -discountRate);
    }

    // Private instance method — shared logic for the two default methods above,
    // CAN call baseValue() because we're still in an instance context:
    private double applyRate(double base, double rate) {
        return base + (base * rate);
    }

    // Public static factory method:
    static Calculator fixed(double value) {
        validate(value);              // calling a private STATIC helper — no instance context needed
        return () -> value;
    }

    // Private STATIC method — no "this", usable only from static contexts
    // (and from instance contexts too, since instance code can call static methods):
    private static void validate(double value) {
        if (value < 0) {
            throw new IllegalArgumentException("Value cannot be negative: " + value);
        }
    }
}

Calculator c = Calculator.fixed(100.0);
System.out.println(c.withTax(0.08));        // 108.0
System.out.println(c.withDiscount(0.10));   // 90.0
// Calculator.validate(5);   // COMPILE ERROR — private static, invisible outside the interface
// c.applyRate(10, 0.1);     // COMPILE ERROR — private instance method, invisible outside the interface

Rules, Restrictions, and Relationship to Java 8's Default/Static Methods

Several restrictions apply specifically to private interface methods. They must always have a body — an interface cannot declare a private method as abstract, since the entire point of a private method is to provide internal, callable implementation logic, and a bodiless private method would be uncallable by definition while also breaking the abstract-method contract model (abstract methods are meant to be implemented by implementing classes, but a private method is precisely not visible to implementing classes, so there would be no way for them to ever provide that implementation). They also cannot be marked default — default and private are mutually exclusive modifiers for an interface method, since default specifically signals "implementing classes inherit this and may override it," which is the opposite of what private means. A private interface method, like a private method in a class, cannot be overridden by anything, anywhere — not by other methods within the same interface (you cannot have two private methods with the same signature for the purpose of "overriding" one with another), and certainly not by implementing classes, since they cannot even see that the private method exists in the first place. This makes private interface methods purely an implementation-sharing tool with zero polymorphic behavior, in contrast to default methods, which are fully part of the dynamic dispatch and diamond-conflict-resolution system covered under default methods. Architecturally, private interface methods complete the symmetry Java 8 began: default methods gave interfaces the ability to provide shared instance-level behavior (like a "soft" abstract class capability), static methods gave interfaces a home for factory/utility logic, and private (instance and static) methods finally gave interfaces the same internal-refactoring tool classes have always had — the ability to factor out shared logic without that logic becoming part of the type's public contract. Together, these four kinds of interface methods (abstract, default, static, private) let interfaces in modern Java carry a meaningfully complete implementation, not merely a contract, while still preserving the core distinctions that separate an interface from an abstract class: no instance fields holding mutable state, support for multiple inheritance, and no constructors.
Java
interface Example {
    // ── Private methods MUST have a body — cannot be abstract ──────────
    // private void noBody();   // COMPILE ERROR: private interface methods cannot be abstract

    // ── private and default are mutually exclusive ──────────────────────
    // private default void both() { }   // COMPILE ERROR: illegal combination of modifiers

    default void doWork() {
        step1();
        step2();
    }

    // ── Private methods cannot be overridden — anywhere, by anyone ─────
    private void step1() {
        System.out.println("Step 1 — internal detail");
    }
    private void step2() {
        System.out.println("Step 2 — internal detail");
    }
}

class Impl implements Example {
    // No step1()/step2() visible here at all — cannot override what cannot be seen.
    // private void step1() { ... }   // this would just be an UNRELATED private method
    //                                   on Impl itself, not an override of anything.
}

// ── The full, completed interface toolkit since Java 8 + Java 9 ────────
interface CompleteToolkit {
    int value();                                  // abstract — the contract

    default int doubled() { return scale(2); }    // defaultpublic, inherited, overridable
    default int tripled() { return scale(3); }

    static CompleteToolkit of(int v) {             // static — factory, not inherited
        checkRange(v);
        return () -> v;
    }

    private int scale(int factor) {                // private instance — shared, hidden helper
        return value() * factor;
    }
    private static void checkRange(int v) {         // private static — shared, hidden helper
        if (v < 0) throw new IllegalArgumentException("negative: " + v);
    }
}
// abstract: the contract | default: shared, inherited, overridable behavior
// static: factory/utility, not inherited | private: internal reuse, invisible outside

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
var Keyword
The var keyword, introduced in Java 10 as local variable type inference, lets a local variable declaration omit its explicit type and have the compiler infer it from the initializer expression on the right-hand side, while the variable remains just as statically typed at compile time as if the type had been written out explicitly — var is purely a source-code convenience, not a shift toward dynamic typing. This is a narrower, more conservative feature than type inference in languages like C# (var) or Kotlin (val/var): it applies only to local variables with an initializer, never to fields, method parameters, or return types, and the compiler still performs full static type checking using the inferred type. This entry covers the motivation and explicit scope restrictions, how type inference actually resolves (including with generics, diamond operator interaction, and anonymous classes), the readability tradeoffs and style guidance the OpenJDK team itself published, and the specific cases where var cannot be used.