cyberangles guide

Kotlin Interfaces: How They Differ from Java

Interfaces are a cornerstone of object-oriented programming (OOP), enabling abstraction, polymorphism, and contract-based design. Both Kotlin and Java support interfaces, but Kotlin extends their capabilities with modern features that enhance flexibility, reduce boilerplate, and simplify common patterns. If you’re familiar with Java interfaces, Kotlin’s take on interfaces will feel both familiar and refreshingly powerful. In this blog, we’ll dive deep into Kotlin interfaces, exploring how they differ from Java interfaces across key dimensions like default methods, properties, static members, and more. By the end, you’ll understand why Kotlin interfaces are a significant upgrade for developers transitioning from Java.

Table of Contents

  1. What is an Interface?
  2. Key Differences Between Kotlin and Java Interfaces
  3. Summary
  4. References

What is an Interface?

An interface defines a contract that classes or other interfaces can implement. It declares methods (and in some cases, constants or properties) that implementing classes must provide, but it does not contain concrete implementations (with exceptions, as we’ll see). Interfaces enable loose coupling by separating “what” a class does from “how” it does it.

In both Java and Kotlin, interfaces support multiple inheritance: a class can implement any number of interfaces. However, Kotlin expands on this foundation with features that address Java’s limitations.

Key Differences Between Kotlin and Java Interfaces

1. Default Methods

Java 8 introduced default methods to interfaces, allowing concrete method implementations with the default keyword. Kotlin also supports default methods but simplifies the syntax by omitting the default keyword entirely.

Java Default Methods

In Java, default methods require the default modifier:

public interface Greetable {
    // Abstract method (must be implemented by classes)
    String getGreeting();

    // Default method (provides a concrete implementation)
    default void greet() {
        System.out.println(getGreeting());
    }
}

Kotlin Default Methods

Kotlin interfaces drop the default keyword—simply define the method with a body:

interface Greetable {
    // Abstract method
    fun getGreeting(): String

    // Default method (no 'default' keyword needed)
    fun greet() {
        println(getGreeting())
    }
}

Conflict Resolution: If a class implements two interfaces with conflicting default methods, both languages require explicit resolution.

  • Java: Use InterfaceName.super.method() to specify which interface’s method to inherit:

    public class DualGreet implements Greetable, FormalGreetable {
        @Override
        public void greet() {
            Greetable.super.greet(); // Explicitly choose Greetable's greet()
        }
    }
  • Kotlin: Use super<InterfaceName>.method() for the same purpose:

    class DualGreet : Greetable, FormalGreetable {
        override fun greet() {
            super<Greetable>.greet() // Explicitly choose Greetable's greet()
        }
    }

2. Properties in Interfaces

Java interfaces are limited to constants ( public static final fields). They cannot declare instance properties or abstract properties. Kotlin, however, allows properties in interfaces—with constraints.

Java: Only Constants

Java interfaces can define constants (implicitly public static final), but no instance properties:

public interface Config {
    // Constant (public static final by default)
    int MAX_RETRIES = 3; 
    
    // ERROR: Cannot declare instance properties
    // String version; 
}

Kotlin: Abstract and Concrete Properties

Kotlin interfaces support properties, but they cannot hold state (no backing fields). Properties must be:

  • Abstract: Declared without an initializer (implementing classes must provide the value).
  • Concrete: Defined with a custom getter (no backing field allowed).

Example: Kotlin Interface Properties

interface Config {
    // Abstract property (must be implemented by classes)
    val version: String

    // Concrete property with a getter (no backing field)
    val maxRetries: Int get() = 3 // Computed on the fly; no stored state

    // ERROR: Cannot have a backing field (initializer not allowed)
    // val minRetries: Int = 1 
}

// Implementing class provides the abstract property
class AppConfig : Config {
    override val version: String = "1.0.0"
}

3. Static Members

Java interfaces explicitly support static methods and constants. Kotlin avoids the static keyword entirely, using companion objects to encapsulate static-like members in interfaces.

Java: Static Methods and Constants

Java interfaces can declare static methods and constants directly:

public interface MathUtils {
    // Static constant
    static final double PI = 3.14159;

    // Static method
    static int square(int num) {
        return num * num;
    }
}

// Usage: Call directly on the interface
MathUtils.square(5); // Returns 25

Kotlin: Companion Objects for Static-Like Members

Kotlin interfaces use companion object blocks to define static-like members (since Kotlin has no static keyword):

interface MathUtils {
    // Companion object replaces static members
    companion object {
        // Compile-time constant (similar to Java's static final)
        const val PI: Double = 3.14159 

        // Static-like method
        fun square(num: Int): Int = num * num
    }
}

// Usage: Call directly on the interface (same as Java)
MathUtils.square(5) // Returns 25

4. Visibility Modifiers

Java interfaces enforce public visibility for all members (methods, constants). Kotlin interfaces support granular visibility modifiers: public (default), internal, or private.

Java: All Members Are Public

Java interfaces have no control over visibility—everything is implicitly public:

public interface Secure {
    // Implicitly public
    void encrypt(String data);

    // Implicitly public static final
    String ALGORITHM = "AES"; 
}

Kotlin: Flexible Visibility

Kotlin interfaces can restrict member visibility:

  • private: Only visible within the interface.
  • internal: Visible within the same module.
  • public: Visible everywhere (default).

Example: Kotlin Interface Visibility

interface Secure {
    // Public by default (visible to all)
    fun encrypt(data: String): String

    // Internal (visible only within the module)
    internal fun validateKey(key: String): Boolean

    // Private (only visible inside this interface)
    private fun logEncryption() {
        println("Encryption performed")
    }
}

5. Functional Interfaces (SAM Interfaces)

A “functional interface” (or SAM interface) has exactly one abstract method (Single Abstract Method). Both languages support SAM conversion (using lambdas to implement the interface), but Kotlin adds explicit syntax.

Java: Implicit SAM Interfaces

Java implicitly treats any interface with one abstract method as a SAM interface. You can use lambdas to implement them:

// SAM interface (one abstract method)
@FunctionalInterface // Optional annotation for clarity
public interface ClickListener {
    void onClick();
}

// Usage with lambda (SAM conversion)
ClickListener listener = () -> System.out.println("Clicked!");

Kotlin: Explicit fun Interfaces

Kotlin requires the fun modifier to mark an interface as a SAM interface. This makes intent clear and enables SAM conversion:

Example: Kotlin SAM Interface

// Explicit SAM interface with 'fun' modifier
fun interface ClickListener {
    fun onClick()
}

// Usage with lambda (SAM conversion)
val listener = ClickListener { println("Clicked!") }

Key Difference: Kotlin only allows SAM conversion for:

  • Java SAM interfaces (no fun modifier needed).
  • Kotlin interfaces marked with fun.

Non-fun Kotlin interfaces with one abstract method do not support SAM conversion.

6. Inheritance and Multiple Interfaces

Both languages allow classes to implement multiple interfaces, but Kotlin simplifies method overrides with the override keyword.

Java: Implicit Overrides

Java does not require the @Override annotation for interface methods (though it’s recommended for clarity):

public class Button implements ClickListener {
    // Override is optional (but good practice)
    public void onClick() { 
        System.out.println("Button clicked"); 
    }
}

Kotlin: Mandatory override

Kotlin enforces the override keyword for all overridden members, preventing accidental method shadowing:

class Button : ClickListener {
    // 'override' is mandatory
    override fun onClick() {
        println("Button clicked")
    }
}

7. Sealed Interfaces

Sealed interfaces restrict which classes/interfaces can implement them, enabling exhaustive when statements. Kotlin and Java both support sealed interfaces, but with syntax and rules differences.

Java: sealed + permits

Java 17 introduced sealed interfaces with the sealed keyword and a permits clause to list allowed implementations:

// Sealed interface: only permits Circle and Square
public sealed interface Shape permits Circle, Square {
    double area();
}

// All implementations must be in the same package
public final class Circle implements Shape { ... }

Kotlin: sealed with Implicit Permits

Kotlin’s sealed interfaces use the sealed keyword and implicitly restrict implementations to the same package (or file, in older Kotlin versions):

// Sealed interface (no 'permits' clause)
sealed interface Shape {
    fun area(): Double
}

// Implementations must be in the same package/module
class Circle(val radius: Double) : Shape {
    override fun area() = Math.PI * radius * radius
}

Key Difference: Kotlin does not require a permits clause—implementations are automatically restricted to the same package/module.

8. Interface Delegation

Kotlin simplifies “delegation” (reusing an interface implementation from another object) with the by keyword. Java requires manual method forwarding.

Java: Manual Delegation

In Java, you must explicitly forward all interface methods to a delegate object:

public interface Printer {
    void print(String text);
}

public class ConsolePrinter implements Printer {
    @Override
    public void print(String text) {
        System.out.println(text);
    }
}

// Wrapper that delegates to ConsolePrinter
public class LoggingPrinter implements Printer {
    private final Printer delegate;

    public LoggingPrinter(Printer delegate) {
        this.delegate = delegate;
    }

    // Manual delegation: forward to delegate
    @Override
    public void print(String text) {
        System.out.println("Logging: " + text);
        delegate.print(text); // Forward call
    }
}

Kotlin: Built-In Delegation with by

Kotlin’s by keyword automatically delegates all interface methods to a delegate object, eliminating boilerplate:

Example: Kotlin Interface Delegation

interface Printer {
    fun print(text: String)
}

class ConsolePrinter : Printer {
    override fun print(text: String) {
        println(text)
    }
}

// Delegate all Printer methods to 'delegate' via 'by'
class LoggingPrinter(delegate: Printer) : Printer by delegate {
    // Override specific methods if needed
    override fun print(text: String) {
        println("Logging: $text")
        super.print(text) // Call delegate's print()
    }
}

Summary

Here’s a quick recap of the key differences:

FeatureJavaKotlin
Default MethodsRequires default keywordNo default keyword; method body suffices
PropertiesOnly public static final constantsAbstract properties or concrete getters (no state)
Static Membersstatic methods/variablesCompanion objects (no static keyword)
VisibilityAll members public by defaultSupports public, internal, private
SAM InterfacesImplicit (one abstract method)Explicit with fun modifier
Override KeywordOptional (@Override annotation)Mandatory (override keyword)
Sealed Interfacessealed permits clausesealed (implicit package restriction)
Interface DelegationManual method forwardingBuilt-in with by keyword

References