Milling Project Coin: Private Methods in Interfaces

Quick Summary:

  • Allows private methods and private static methods in interfaces, facilitating code reuse within the interface without exposing implementation details.
  • Released in Java 9 (2017).
    • Part of “Milling Project Coin” (JEP 213).

Default Methods Refresher

To fully appreciate the benefits of private methods in interfaces, it’s crucial to first understand the role of default methods, released in Java 8 (2014) with JSR 335.

  • Backwards Compatibility: Default methods were introduced to enable the addition of new methods to existing interfaces without breaking existing usage of said interface. This allows libraries to evolve while maintaining backward compatibility with older versions of the interface.
  • Smooth API Updates: By using default methods, API developers can add new methods to interfaces without forcing all implementing classes to immediately implement the new methods. This provides a grace period during which existing codebases can continue to function without sudden compilation errors.

Private Methods in Interfaces

Now you can have private “helper” methods in your interfaces so that repeating logic can be tucked away and re-used in private (non-exposed) methods only useful for internal use in the interface.

Key Features

  • Encapsulation: Private methods in interfaces help encapsulate helper method logic that doesn’t need to be exposed outside of the interface.
  • Code Reusability: Enables code reusability within interfaces, allowing multiple default methods to share common code.
  • Maintenance and Readability: Improves code maintenance and readability by keeping utility methods private within interfaces.

Examples

Before Java 11:

Before Java 9, all methods in an interface were either public or default. Common functionality needed by multiple default methods had to be duplicated or exposed unnecessarily.

public interface BeforeDemo {
    default void sayHello(){
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
        LocalDateTime now = LocalDateTime.now();
        System.out.printf("%s: Hello there.\n", now.format(formatter));
    }
    
    default void sayGoodbye(){ // Lots of code repetition here 😢
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
        LocalDateTime now = LocalDateTime.now();
        System.out.printf("%s: Goodbye.\n", now.format(formatter));
    }
}

Or…

public interface BeforeDemo {
    default void sayHello(){
        printWithTimestamp("Hello there.");
    }

    default void sayGoodbye(){
        printWithTimestamp("Goodbye.");
    }

    // Common code is exposed to the public despite being for internal use only 😢
    default void printWithTimestamp(String msg){
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
        LocalDateTime now = LocalDateTime.now();
        System.out.printf("%s: %s\n", now.format(formatter), msg);
    }
}

Both solutions are not ideal; we don’t want duplicate code, and we certainly don’t want to expose methods that are only there for internal use to avoid code repetition.

After Java 11:

We can avoid duplicate code and having to expose new public methods purely for tucking away common logic by using private methods, designed for internal use!

public interface AfterDemo {
    default void sayHello(){
        printWithTimestamp("Hello there.");
    }

    default void sayGoodbye(){
        printWithTimestamp("Goodbye.");
    }

    // Common code is kept private for internal use only 🤩
    private void printWithTimestamp(String msg){
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
        LocalDateTime now = LocalDateTime.now();
        System.out.printf("%s: %s\n", now.format(formatter), msg);
    }
}

👩‍💻 Hands-on Demo: Private Methods in Interfaces

  1. Create 2 interfaces, BeforeDemo and AfterDemo, and re-create the code shown in the examples above, illustrating the benefits of private methods.
  2. In a new Calculator-interface, have normal public abstract methods such as:
    • add(a: double, b: double): double
    • subtract(a: double, b: double): double
    • multiply(a: double, b: double): double
    • divide(a: double, b: double): double
  3. Implement these methods in a CalculatorImpl-class and use them in the main-method of a CalcApp-class.
  4. Simulate a version 1.1 update to the Calculator-interface, by adding 2 new methods:
    • square(a: double): double
    • cube(a: double): double
  5. Ensure a smooth update for developers using of your API by providing a default implementation for these 2 methods (you should be able to re-run CalcApp without issue).
  6. Refactor your Calculator-interface by getting rid of repeating logic, placing common logic in a reusable exponentiate-method, but make sure it isn’t exposed (i.e. for internal use only), thereby keeping usage of your interface intact.
  7. Notice how, thanks to the default implementations and private method, you were able to add new methods without breaking the rest of your application’s use of the interface, plus you were able to write clean, reusable code for internal use.
  8. Add the use of the square– and cube-methods in the CalcApp-class without overriding them in the implementing class.

Solutions

🕵️‍♂️ Click here to reveal the solutions
public interface Calculator {
    double add(double a, double b);
    double subtract(double a, double b);
    double multiply(double a, double b);
    double divide(double a, double b);
    default double square(double a){
        return exponentiate(a, 2);
    }
    default double cube(double a){
        return exponentiate(a, 3);
    }

    private double exponentiate(double base, int exponent){
        double total = 1;
        for(int i = 0; i < exponent; i++){
            total *= base;
        }
        return total;
    }
}
public class CalculatorImpl implements Calculator {
    @Override
    public double add(double a, double b) {
        return a + b;
    }

    @Override
    public double subtract(double a, double b) {
        return a - b;
    }

    @Override
    public double multiply(double a, double b) {
        return a * b;
    }

    @Override
    public double divide(double a, double b) {
        return a / b;
    }
}
public class CalcApp {
    public static void main(String[] args) {
        Calculator calculator = new CalculatorImpl();
        double x = 2;
        double y = 3;
        System.out.println(calculator.add(x, y));
        System.out.println(calculator.subtract(x, y));
        System.out.println(calculator.multiply(x, y));
        System.out.println(calculator.divide(x, y));
        System.out.println(calculator.square(x));
        System.out.println(calculator.cube(x));
    }
}

Summary

  • Introduction of Private Methods: Java 9’s JEP 213 added private methods to interfaces, enhancing encapsulation by allowing internal helper methods not to be exposed as part of the public API.
  • Builds on Default Methods: Extends Java 8’s default methods, which facilitated adding new functionalities to interfaces without breaking existing implementations, thereby maintaining backward compatibility.
  • Enhances Code Reusability and Maintenance: Enables shared common code between default methods, reducing redundancy and improving code maintainability.
  • Practical Impact: Prevents code duplication and unnecessary public exposure of internal methods, streamlining interface design for better clarity and usability.