Kotlin - Interfaces & Abstract Classes
Overview
Interfaces and abstract classes in Kotlin provide powerful mechanisms for defining contracts and sharing common functionality across different classes. This tutorial covers interface declarations, default implementations, abstract classes, and best practices for contract-based programming.
๐ฏ Learning Objectives:
- Understand the difference between interfaces and abstract classes
- Learn to declare and implement interfaces with default methods
- Master abstract classes and their use cases
- Explore multiple interface implementation and conflict resolution
- Apply interface segregation and dependency inversion principles
Interfaces in Kotlin
Interfaces define contracts that classes must implement. Unlike Java 7, Kotlin interfaces can contain default implementations.
Basic Interface Declaration
// Simple interface
interface Drawable {
fun draw()
fun getArea(): Double
}
// Interface with properties
interface Shape {
val name: String
val perimeter: Double
get() = 0.0 // Default implementation
fun calculateArea(): Double
fun displayInfo() {
println("Shape: $name, Area: ${calculateArea()}")
}
}
// Implementation
class Circle(private val radius: Double) : Shape {
override val name = "Circle"
override val perimeter: Double
get() = 2 * Math.PI * radius
override fun calculateArea(): Double = Math.PI * radius * radius
}
fun main() {
val circle = Circle(5.0)
circle.displayInfo() // Shape: Circle, Area: 78.53981633974483
println("Perimeter: ${circle.perimeter}") // Perimeter: 31.41592653589793
}
Multiple Interface Implementation
interface Flyable {
fun fly() {
println("Flying through the air")
}
}
interface Swimmable {
fun swim() {
println("Swimming in water")
}
}
interface Walkable {
fun walk()
}
// Duck implements multiple interfaces
class Duck : Flyable, Swimmable, Walkable {
override fun walk() {
println("Duck walking on land")
}
// Can override default implementations
override fun fly() {
println("Duck flying with flapping wings")
}
}
// Platypus with selective abilities
class Platypus : Swimmable, Walkable {
override fun walk() {
println("Platypus walking with webbed feet")
}
}
fun main() {
val duck = Duck()
duck.fly() // Duck flying with flapping wings
duck.swim() // Swimming in water
duck.walk() // Duck walking on land
val platypus = Platypus()
platypus.swim() // Swimming in water
platypus.walk() // Platypus walking with webbed feet
}
Resolving Interface Conflicts
interface A {
fun foo() {
println("A's foo")
}
}
interface B {
fun foo() {
println("B's foo")
}
}
// Must resolve the conflict
class C : A, B {
override fun foo() {
super.foo() // Call A's implementation
super.foo() // Call B's implementation
println("C's foo")
}
}
fun main() {
val c = C()
c.foo()
// Output:
// A's foo
// B's foo
// C's foo
}
Abstract Classes
Abstract classes cannot be instantiated and are designed to be extended. They can contain both abstract and concrete members.
Basic Abstract Class
abstract class Animal {
// Concrete property
val isAlive = true
// Abstract property
abstract val species: String
// Concrete method
fun sleep() {
println("$species is sleeping")
}
// Abstract method
abstract fun makeSound()
// Method with default implementation that can be overridden
open fun move() {
println("$species is moving")
}
}
class Dog(override val species: String = "Canine") : Animal() {
override fun makeSound() {
println("Woof! Woof!")
}
override fun move() {
println("Dog is running on four legs")
}
}
class Bird(override val species: String = "Avian") : Animal() {
override fun makeSound() {
println("Tweet! Tweet!")
}
override fun move() {
println("Bird is flying with wings")
}
}
fun main() {
val dog = Dog()
dog.makeSound() // Woof! Woof!
dog.move() // Dog is running on four legs
dog.sleep() // Canine is sleeping
val bird = Bird()
bird.makeSound() // Tweet! Tweet!
bird.move() // Bird is flying with wings
}
Abstract Classes with Constructors
abstract class Vehicle(val brand: String, val model: String) {
// Abstract properties
abstract val maxSpeed: Int
abstract val fuelType: String
// Concrete method
fun displayInfo() {
println("$brand $model - Max Speed: ${maxSpeed}km/h, Fuel: $fuelType")
}
// Abstract method
abstract fun start()
abstract fun stop()
}
class Car(
brand: String,
model: String,
override val maxSpeed: Int,
override val fuelType: String = "Gasoline"
) : Vehicle(brand, model) {
override fun start() {
println("$brand $model engine started")
}
override fun stop() {
println("$brand $model engine stopped")
}
}
class ElectricCar(
brand: String,
model: String,
override val maxSpeed: Int,
val batteryCapacity: Int
) : Vehicle(brand, model) {
override val fuelType = "Electric"
override fun start() {
println("$brand $model electric motor activated")
}
override fun stop() {
println("$brand $model electric motor deactivated")
}
fun chargeBattery() {
println("Charging battery (${batteryCapacity}kWh capacity)")
}
}
fun main() {
val car = Car("Toyota", "Camry", 180)
car.displayInfo() // Toyota Camry - Max Speed: 180km/h, Fuel: Gasoline
car.start() // Toyota Camry engine started
val tesla = ElectricCar("Tesla", "Model 3", 225, 75)
tesla.displayInfo() // Tesla Model 3 - Max Speed: 225km/h, Fuel: Electric
tesla.chargeBattery() // Charging battery (75kWh capacity)
}
Real-World Example: Payment Processing System
// Payment interface defining the contract
interface PaymentProcessor {
val processingFee: Double
fun processPayment(amount: Double): PaymentResult
fun refund(transactionId: String, amount: Double): RefundResult
// Default implementation for fee calculation
fun calculateTotalAmount(amount: Double): Double {
return amount + (amount * processingFee)
}
}
// Additional interface for digital wallets
interface DigitalWallet {
val walletBalance: Double
fun checkBalance(): Double
fun addFunds(amount: Double)
}
// Data classes for results
data class PaymentResult(val success: Boolean, val transactionId: String, val message: String)
data class RefundResult(val success: Boolean, val refundId: String, val message: String)
// Abstract base class for payment methods
abstract class PaymentMethod(val provider: String) {
abstract fun validatePayment(amount: Double): Boolean
protected fun generateTransactionId(): String {
return "TXN_${System.currentTimeMillis()}"
}
protected fun log(message: String) {
println("[$provider] $message")
}
}
// Concrete implementation: Credit Card
class CreditCardProcessor(
provider: String,
private val cardNumber: String
) : PaymentMethod(provider), PaymentProcessor {
override val processingFee = 0.029 // 2.9%
override fun validatePayment(amount: Double): Boolean {
return amount > 0 && cardNumber.length == 16
}
override fun processPayment(amount: Double): PaymentResult {
if (!validatePayment(amount)) {
return PaymentResult(false, "", "Invalid payment details")
}
val transactionId = generateTransactionId()
val totalAmount = calculateTotalAmount(amount)
log("Processing credit card payment: $${totalAmount}")
// Simulate payment processing
return PaymentResult(true, transactionId, "Payment successful")
}
override fun refund(transactionId: String, amount: Double): RefundResult {
log("Processing refund for transaction: $transactionId")
return RefundResult(true, "REF_${System.currentTimeMillis()}", "Refund processed")
}
}
// Digital wallet implementation
class PayPalProcessor : PaymentMethod("PayPal"), PaymentProcessor, DigitalWallet {
override val processingFee = 0.034 // 3.4%
private var _walletBalance = 1000.0
override val walletBalance: Double
get() = _walletBalance
override fun validatePayment(amount: Double): Boolean {
return amount > 0 && walletBalance >= calculateTotalAmount(amount)
}
override fun processPayment(amount: Double): PaymentResult {
if (!validatePayment(amount)) {
return PaymentResult(false, "", "Insufficient balance or invalid amount")
}
val totalAmount = calculateTotalAmount(amount)
_walletBalance -= totalAmount
val transactionId = generateTransactionId()
log("PayPal payment processed: $${totalAmount}, Remaining balance: $${walletBalance}")
return PaymentResult(true, transactionId, "PayPal payment successful")
}
override fun refund(transactionId: String, amount: Double): RefundResult {
_walletBalance += amount
log("Refund processed: $${amount}, New balance: $${walletBalance}")
return RefundResult(true, "REF_${System.currentTimeMillis()}", "Refund added to wallet")
}
override fun checkBalance(): Double = walletBalance
override fun addFunds(amount: Double) {
_walletBalance += amount
log("Added funds: $${amount}, New balance: $${walletBalance}")
}
}
// Usage example
fun main() {
val creditCard = CreditCardProcessor("Visa", "1234567890123456")
val paypal = PayPalProcessor()
// Process payments
val ccResult = creditCard.processPayment(100.0)
println("Credit Card: ${ccResult.message}")
val ppResult = paypal.processPayment(50.0)
println("PayPal: ${ppResult.message}")
// Check wallet balance
if (paypal is DigitalWallet) {
println("PayPal balance: $${paypal.checkBalance()}")
}
// Process refund
val refund = paypal.refund(ppResult.transactionId, 50.0)
println("Refund: ${refund.message}")
}
Interface vs Abstract Class Guidelines
๐ When to Use Interfaces:
- Define contracts that multiple unrelated classes can implement
- Support multiple inheritance of type
- Provide flexibility in implementation
- Follow "program to interface, not implementation" principle
๐ When to Use Abstract Classes:
- Share common implementation among related classes
- Provide default behavior that can be inherited
- Define a base class with some concrete and some abstract members
- Control access to constructors and initialization
Common Pitfalls
- Diamond Problem: Be careful with multiple interface inheritance with conflicting default methods
- Interface Pollution: Keep interfaces focused and cohesive (Interface Segregation Principle)
- Over-abstraction: Don't create abstract classes if you only need a contract
- Property Conflicts: Be explicit when resolving property conflicts in multiple inheritance
Practice Exercises
- Create a
Sortable
interface with a defaultquickSort()
method - Design an abstract
DatabaseConnection
class with concrete and abstract methods - Implement a plugin system using interfaces for different plugin types
- Create a notification system with multiple delivery methods (Email, SMS, Push)
Quick Quiz
- What's the difference between an interface and an abstract class in Kotlin?
- How do you resolve conflicts when implementing multiple interfaces with the same method signature?
- Can an interface have properties with backing fields?
Show Answers
- Interfaces define contracts and can have default implementations but no state (backing fields). Abstract classes can have both abstract and concrete members, constructors, and state.
- Use
super<InterfaceName>.methodName()
to explicitly call a specific interface's implementation. - No, interface properties cannot have backing fields, only custom getters/setters or delegation.