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 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 allPrivate Instance Methods vs Private Static Methods in Interfaces
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 interfaceRules, Restrictions, and Relationship to Java 8's Default/Static Methods
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); } // default — public, 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