Default, Private, and Static Methods in Java Interfaces

Reetesh Kumar
8 min readOct 23, 2023

--

Introduction

When Java introduced the concept of interfaces, it provided a powerful mechanism for achieving multiple inheritance and defining contracts for classes. In the past, interfaces were limited to defining method signatures that classes implementing the interface had to provide. However, with the release of Java 8, interfaces got a significant upgrade. Default, private(Java 9), and static methods were introduced to interfaces, making them more flexible and versatile. In this blog, we will explore these enhancements with examples in Java.

The Basics of Java Interfaces

In Java, an interface is a way to define a contract or a set of methods that a class must implement. An interface declares a set of method signatures without providing any implementation for those methods. Classes that implement an interface must provide concrete implementations for all the methods declared in that interface. Interfaces are often used to achieve abstraction, to define common behavior for different classes, and to support multiple inheritance.

Creating a simple example of how to onboard a customer to a bank using Java interfaces and classes:

// Define a Customer interface
interface Customer {
void openAccount();
void closeAccount();
void deposit(double amount);
void withdraw(double amount);
}

// Create a class for the Bank
class Bank {
private String name;

public Bank(String name) {
this.name = name;
}

public void welcomeCustomer(Customer customer) {
System.out.println("Welcome to " + name + ", " + customer.getClass().getSimpleName() + "!");
customer.openAccount();
}
}

// Create a class for a SavingsAccount customer
class SavingsAccountCustomer implements Customer {
private double balance;

public SavingsAccountCustomer() {
balance = 0;
}

@Override
public void openAccount() {
System.out.println("Savings Account opened.");
}

@Override
public void closeAccount() {
System.out.println("Savings Account closed.");
}

@Override
public void deposit(double amount) {
balance += amount;
System.out.println("Deposited $" + amount + ". New balance: $" + balance);
}

@Override
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
System.out.println("Withdrawn $" + amount + ". New balance: $" + balance);
} else {
System.out.println("Insufficient funds.");
}
}
}

public class Main {
public static void main(String[] args) {
Bank myBank = new Bank("MyBank");

SavingsAccountCustomer customer1 = new SavingsAccountCustomer();
myBank.welcomeCustomer(customer1);

customer1.deposit(1000);
customer1.withdraw(200);
customer1.closeAccount();
}
}

In this example:

We define a Customer interface that includes methods for opening an account, closing an account, depositing money, and withdrawing money.

The Bank class has a welcomeCustomer method that accepts a Customer object and welcomes them to the bank by calling the openAccount method. It also has a name attribute to identify the bank’s name.

The SavingsAccountCustomer class implements the Customer interface and provides concrete implementations for the methods. It also maintains a balance to track the account balance.

In the Main class, we create a Bank object and a SavingsAccountCustomer object, and we welcome the customer to the bank. We then perform operations such as deposit, withdrawal, and account closure for this customer.

Default Methods

Default methods were introduced in Java 8 to allow interfaces to have concrete method implementations. This was a significant change to the Java language, as interfaces had previously only been able to define abstract methods. Default methods are defined using the default keyword.

But why default methods were introduced?

There are several reasons why default methods were added to Java Interface:

  1. Backward Compatibility: To allow interfaces to evolve without breaking existing code. Prior to Java 8, if a new method was added to an interface, all classes that implemented the interface had to be updated to provide an implementation for the new method. This could be a major undertaking, especially for large codebases. With default methods, new methods can be added to interfaces without requiring changes to existing code.
  2. Common Functionality: To provide a way for interfaces to define common functionality. Default methods can be used to define common functionality that is shared by all classes that implement the interface. This can reduce the amount of code that needs to be written in each class.
  3. Flexibility: To make interfaces more flexible. Default methods allow interfaces to be more flexible, as they can now provide concrete implementations for methods. This can make it easier to use interfaces in a variety of situations.

For example, the java.util.List interface defines a default method called sort(). This method can be used to sort a list of elements. Prior to Java 8, if a class wanted to sort a list, it would have to provide its own implementation of the sort() method. With Java 8, classes can simply call the sort() method on the list. This can save a significant amount of time and effort.

Let’s explore the concepts of default methods in Java interfaces through a practical example using a Bank interface. In this example, we'll design an interface for a simple banking system with account-related methods.

interface Bank {
// Abstract methods
double getBalance();
void deposit(double amount);
void withdraw(double amount);

// Default method to display account type
default void displayAccountType() {
System.out.println("Account Type: Generic");
}

}

class SavingsAccount implements Bank {
private double balance;

public SavingsAccount(double initialBalance) {
this.balance = initialBalance;
}

@Override
public double getBalance() {
return balance;
}

@Override
public void deposit(double amount) {
balance += amount;
}

@Override
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
} else {
System.out.println("Insufficient funds.");
}
}

// Overriding the default method to display a specific account type
@Override
public void displayAccountType() {
System.out.println("Account Type: Savings Account");
}
}

public class BankExample {
public static void main(String[] args) {
// Create a savings account using the static method
Bank account = new SavingsAccount(1000);

// Access and display the account type
account.displayAccountType();

// Deposit and withdraw funds
account.deposit(500);
account.withdraw(200);

// Display the final balance
System.out.println("Final Balance: $" + account.getBalance());
}
}

// Output
Account Type: Savings Account
Final Balance: $1300.0

In the above example:

We define a Bank interface with three abstract methods (getBalance, deposit, and withdraw) that represent common actions for a bank account.

The displayAccountType method is a default method that provides a generic account-type display. Implementing classes can override this method to specify the account type.

We create a SavingsAccount class that implements the Bank interface. It provides specific implementations for the abstract methods and overrides the displayAccountType method to display the account type as “Savings Account.”

In the main method, we use the static createAccount method to create a SavingsAccount instance, then demonstrate depositing and withdrawing funds, as well as displaying the account type and final balance.

Private Methods

Private methods in Java interfaces were introduced in Java 9 to provide a way to encapsulate helper code that is not part of the public API of the interface.

This can be useful for a number of reasons:

  1. Code Reusability: Private methods in interfaces can be used to encapsulate and reuse code within the interface, making it more maintainable and avoiding code duplication.
  2. Improved Readability: Private methods allow us to break down complex logic in the interface into smaller, more manageable pieces.

Let’s illustrate this concept with a Bank interface:

interface Bank {
double getBalance();
void deposit(double amount);
void withdraw(double amount);

// Default method to execute a transaction
default void executeTransaction(String type, double amount) {
if (type.equals("DEPOSIT")) {
deposit(amount);
logTransaction(type, amount);
} else if (type.equals("WITHDRAW") && sufficientFunds(amount)) {
withdraw(amount);
logTransaction(type, amount);
} else {
System.out.println("Transaction failed. Insufficient funds.");
}
}

// Private method to log transactions
private void logTransaction(String transactionType, double amount) {
System.out.println("Transaction: " + transactionType);
System.out.println("Amount: $" + amount);
System.out.println("Current Balance: $" + getBalance());
}

// Private method to check for sufficient funds
private boolean sufficientFunds(double amount) {
return getBalance() >= amount;
}
}

class SavingsAccount implements Bank {
private double balance;

public SavingsAccount(double initialBalance) {
this.balance = initialBalance;
}

@Override
public double getBalance() {
return balance;
}

@Override
public void deposit(double amount) {
balance += amount;
}

@Override
public void withdraw(double amount) {
balance -= amount;
}
}

public class BankPrivateMethodExample {
public static void main(String[] args) {
Bank account = new SavingsAccount(1000);

// Execute transactions
account.executeTransaction("DEPOSIT", 500);
account.executeTransaction("WITHDRAW", 200);
account.executeTransaction("WITHDRAW", 2000); // Should fail due to insufficient funds

System.out.println("Final Balance: $" + account.getBalance());
}
}


// Output
Transaction: DEPOSIT
Amount: $500.0
Current Balance: $1500.0
Transaction: WITHDRAW
Amount: $200.0
Current Balance: $1300.0
Transaction failed. Insufficient funds.
Final Balance: $1300.0

In the above example:

The Bank interface has two private methods: logTransaction and sufficientFunds. The logTransaction method is responsible for logging transaction details. The sufficientFunds method checks if the account has enough funds for withdrawal.

Instead of directly invoking the deposit and withdraw methods, we’ve provided a default method executeTransaction that takes in a transaction type and amount. It uses private methods to check funds availability and log transactions.

The SavingsAccount class implements the Bank interface and provides specific implementations for the abstract methods.

In the main method, we demonstrate using the executeTransaction method to handle various transactions and display the final balance.

Static Methods

Static methods were introduced in Java 8 to allow interfaces to define methods that belong to the interface itself, not to any instance of the interface.

This can be useful for a number of reasons:

  1. To provide a way to define utility methods that are related to the interface. For example, the java.util.List interface defines a static method called of(). This method can be used to create a new list containing a specified set of elements.
  2. To provide a way to define factory methods that can be used to create instances of classes that implement the interface. For example, the java.util.Optional interface defines a static method called ofNullable(). This method can be used to create a new Optional instance that contains a specified value, or an empty Optional instance if the value is null.
  3. To provide a way to define methods that are not related to any specific instance of the interface. For example, the java.lang.Math interface defines a static method called abs(). This method can be used to calculate the absolute value of a number.

Let’s illustrate this concept with a Bank interface and a static method for creating a bank account:

interface Bank {
double getBalance();
void deposit(double amount);
void withdraw(double amount);

// Static method to create a bank account
static Bank createAccount(double initialBalance) {
return new SavingsAccount(initialBalance);
}
}

class SavingsAccount implements Bank {
private double balance;

public SavingsAccount(double initialBalance) {
this.balance = initialBalance;
}

@Override
public double getBalance() {
return balance;
}

@Override
public void deposit(double amount) {
balance += amount;
}

@Override
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
} else {
System.out.println("Insufficient funds.");
}
}
}

public class BankStaticMethodExample {
public static void main(String[] args) {
// Create a savings account using the static method
Bank account = Bank.createAccount(1000);

// Deposit and withdraw funds
account.deposit(500);
account.withdraw(200);

// Display the final balance
System.out.println("Final Balance: $" + account.getBalance());
}
}

// Output
Final Balance: $1300.0

In the above example:

The Bank interface has a static method createAccount, which allows the creation of a new bank account. This method is associated with the interface itself, not with any specific instance of the implementing class.

The SavingsAccount class implements the Bank interface and provides specific implementations for the abstract methods.

In the main method, we use the static createAccount method to create a SavingsAccount instance, then demonstrate depositing and withdrawing funds, as well as displaying the final balance.

Conclusion

Java interfaces have evolved with the introduction of default, private, and static methods, allowing for more robust and flexible code design. These enhancements have made interfaces more powerful and versatile, enabling developers to create cleaner and more maintainable code in Java.

Happy Learning !!!

--

--

Reetesh Kumar

Software developer. Writing about Java, Spring, Cloud and new technologies. LinkedIn: www.linkedin.com/in/reeteshkumar1