Interfaces and Abstract Classes
Overview
Interfaces and abstract classes are important concepts in Kotlin object-oriented programming. They provide capabilities for code abstraction, polymorphism, and contract definition. This chapter will detail interface definition, implementation, abstract class usage, and their practical applications.
Interface Basics
Interface Definition and Implementation
kotlin
// Basic interface definition
interface Drawable {
// Abstract method
fun draw()
// Method with default implementation
fun getInfo(): String = "This is a drawable object"
// Abstract property
val color: String
// Property with default implementation
val isVisible: Boolean
get() = true
}
// Implementing interface
class Circle(override val color: String, val radius: Double) : Drawable {
override fun draw() {
println("Drawing a $color circle with radius $radius")
}
override fun getInfo(): String {
return "Circle: color=$color, radius=$radius"
}
}
class Rectangle(override val color: String, val width: Double, val height: Double) : Drawable {
override fun draw() {
println("Drawing a $color rectangle, ${width}x${height}")
}
// Override property
override val isVisible: Boolean = width > 0 && height > 0
}
fun main() {
val shapes: List<Drawable> = listOf(
Circle("red", 5.0),
Rectangle("blue", 10.0, 8.0),
Rectangle("green", 0.0, 5.0) // Invisible rectangle
)
shapes.forEach { shape ->
println(shape.getInfo())
println("Visibility: ${shape.isVisible}")
shape.draw()
println()
}
}Multiple Interface Implementation
kotlin
interface Movable {
fun move(x: Int, y: Int)
val speed: Double
}
interface Rotatable {
fun rotate(angle: Double)
val rotationSpeed: Double
get() = 1.0
}
interface Scalable {
fun scale(factor: Double)
}
// Implementing multiple interfaces
class GameObject(
override val color: String,
override val speed: Double
) : Drawable, Movable, Rotatable, Scalable {
private var x = 0
private var y = 0
private var rotation = 0.0
private var scaleX = 1.0
private var scaleY = 1.0
override fun draw() {
println("Drawing game object at position ($x, $y), rotation ${rotation} degrees, scale ${scaleX}x${scaleY}")
}
override fun move(x: Int, y: Int) {
this.x += x
this.y += y
println("Moved to position (${this.x}, ${this.y})")
}
override fun rotate(angle: Double) {
rotation += angle
println("Rotated ${angle} degrees, current angle ${rotation} degrees")
}
override fun scale(factor: Double) {
scaleX *= factor
scaleY *= factor
println("Scaled ${factor}x, current scale ${scaleX}x${scaleY}")
}
fun getPosition() = Pair(x, y)
fun getRotation() = rotation
fun getScale() = Pair(scaleX, scaleY)
}
fun main() {
val gameObject = GameObject("purple", 5.0)
// Use features from different interfaces
gameObject.draw()
gameObject.move(10, 20)
gameObject.rotate(45.0)
gameObject.scale(1.5)
gameObject.draw()
// Polymorphism
val drawable: Drawable = gameObject
val movable: Movable = gameObject
val rotatable: Rotatable = gameObject
println("As Drawable: ${drawable.getInfo()}")
println("Movement speed: ${movable.speed}")
println("Rotation speed: ${rotatable.rotationSpeed}")
}Advanced Interface Features
Resolving Method Conflicts in Interfaces
kotlin
interface A {
fun foo() {
println("A.foo()")
}
fun bar()
}
interface B {
fun foo() {
println("B.foo()")
}
fun bar() {
println("B.bar()")
}
}
// Resolving method conflicts
class C : A, B {
override fun foo() {
super<A>.foo() // Call A's implementation
super<B>.foo() // Call B's implementation
println("C.foo()") // Own implementation
}
override fun bar() {
super<B>.bar() // Choose B's implementation
}
}
fun main() {
val c = C()
c.foo()
c.bar()
}Functional Interfaces (SAM Interfaces)
kotlin
// Functional interface (Single Abstract Method)
fun interface StringProcessor {
fun process(input: String): String
}
fun interface NumberValidator {
fun validate(number: Int): Boolean
}
fun interface EventHandler<T> {
fun handle(event: T)
}
// Using functional interfaces
class TextProcessor {
fun processText(text: String, processor: StringProcessor): String {
return processor.process(text)
}
fun validateNumbers(numbers: List<Int>, validator: NumberValidator): List<Int> {
return numbers.filter { validator.validate(it) }
}
}
fun main() {
val textProcessor = TextProcessor()
// Implementing functional interface with lambda expression
val upperCaseProcessor = StringProcessor { it.uppercase() }
val trimProcessor = StringProcessor { it.trim() }
val text = " hello world "
println("Original: '$text'")
println("Uppercase: '${textProcessor.processText(text, upperCaseProcessor)}'")
println("Trimmed: '${textProcessor.processText(text, trimProcessor)}'")
// Number validation
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evenValidator = NumberValidator { it % 2 == 0 }
val positiveValidator = NumberValidator { it > 0 }
println("Even numbers: ${textProcessor.validateNumbers(numbers, evenValidator)}")
println("Positive numbers: ${textProcessor.validateNumbers(numbers, positiveValidator)}")
// Event handling
val stringHandler: EventHandler<String> = EventHandler { event ->
println("Handling string event: $event")
}
val intHandler: EventHandler<Int> = EventHandler { event ->
println("Handling integer event: $event")
}
stringHandler.handle("User login")
intHandler.handle(404)
}Abstract Classes
Abstract Class Definition and Usage
kotlin
// Abstract class
abstract class Animal(val name: String, val species: String) {
// Concrete property
var age: Int = 0
// Abstract property
abstract val habitat: String
// Concrete methods
fun eat(food: String) {
println("$name is eating $food")
}
fun sleep() {
println("$name is sleeping")
}
// Abstract methods
abstract fun makeSound()
abstract fun move()
// Open method with default implementation
open fun getInfo(): String {
return "$name is a $species, age $age years old, habitat: $habitat"
}
}
// Inheriting abstract class
class Dog(name: String) : Animal(name, "canine") {
override val habitat = "land"
override fun makeSound() {
println("$name barks")
}
override fun move() {
println("$name is running")
}
// Unique method
fun wagTail() {
println("$name wags tail")
}
}
class Fish(name: String, val waterType: String) : Animal(name, "fish") {
override val habitat = "$waterType water"
override fun makeSound() {
println("$name blows bubbles")
}
override fun move() {
println("$name is swimming")
}
override fun getInfo(): String {
return super.getInfo() + ", water type: $waterType"
}
fun swim() {
println("$name is swimming in water")
}
}
class Bird(name: String, val canFly: Boolean) : Animal(name, "bird") {
override val habitat = "sky and trees"
override fun makeSound() {
println("$name chirps")
}
override fun move() {
if (canFly) {
println("$name is flying")
} else {
println("$name is walking on ground")
}
}
fun fly() {
if (canFly) {
println("$name soars high")
} else {
println("$name cannot fly")
}
}
}
fun main() {
val animals: List<Animal> = listOf(
Dog("Buddy").apply { age = 3 },
Fish("Goldie", "freshwater").apply { age = 1 },
Bird("Tweety", true).apply { age = 2 },
Bird("Penguin", false).apply { age = 5 }
)
println("Zoo information:")
animals.forEach { animal ->
println(animal.getInfo())
animal.makeSound()
animal.move()
animal.eat("food")
// Type checking and casting
when (animal) {
is Dog -> animal.wagTail()
is Fish -> animal.swim()
is Bird -> animal.fly()
}
println()
}
}Combining Abstract Classes and Interfaces
kotlin
// Interface defining behavior contract
interface Flyable {
val maxAltitude: Double
fun takeOff()
fun land()
fun fly(altitude: Double) {
if (altitude <= maxAltitude) {
println("Flying at altitude: ${altitude} meters")
} else {
println("Exceeds maximum flight altitude!")
}
}
}
interface Swimmable {
val maxDepth: Double
fun dive()
fun surface()
fun swim(depth: Double) {
if (depth <= maxDepth) {
println("Swimming at depth: ${depth} meters")
} else {
println("Exceeds maximum dive depth!")
}
}
}
// Abstract class providing base implementation
abstract class Vehicle(val name: String, val maxSpeed: Double) {
var currentSpeed = 0.0
protected set
abstract val fuelType: String
abstract fun start()
abstract fun stop()
open fun accelerate(speed: Double) {
currentSpeed = minOf(currentSpeed + speed, maxSpeed)
println("$name accelerated to ${currentSpeed} km/h")
}
open fun brake(speed: Double) {
currentSpeed = maxOf(currentSpeed - speed, 0.0)
println("$name decelerated to ${currentSpeed} km/h")
}
}
// Airplane: both a vehicle and can fly
class Airplane(name: String, maxSpeed: Double, override val maxAltitude: Double)
: Vehicle(name, maxSpeed), Flyable {
override val fuelType = "aviation fuel"
private var isFlying = false
override fun start() {
println("$name engine starting")
}
override fun stop() {
println("$name engine stopping")
currentSpeed = 0.0
}
override fun takeOff() {
if (!isFlying) {
isFlying = true
println("$name taking off")
}
}
override fun land() {
if (isFlying) {
isFlying = false
println("$name landing")
}
}
}
// Submarine: both a vehicle and can swim
class Submarine(name: String, maxSpeed: Double, override val maxDepth: Double)
: Vehicle(name, maxSpeed), Swimmable {
override val fuelType = "nuclear fuel"
private var isSubmerged = false
override fun start() {
println("$name nuclear reactor starting")
}
override fun stop() {
println("$name nuclear reactor stopping")
currentSpeed = 0.0
}
override fun dive() {
if (!isSubmerged) {
isSubmerged = true
println("$name diving")
}
}
override fun surface() {
if (isSubmerged) {
isSubmerged = false
println("$name surfacing")
}
}
}
// Amphibious vehicle: can drive on land and in water
class AmphibiousVehicle(name: String, maxSpeed: Double, override val maxDepth: Double)
: Vehicle(name, maxSpeed), Swimmable {
override val fuelType = "gasoline"
override fun start() {
println("$name engine starting")
}
override fun stop() {
println("$name engine stopping")
currentSpeed = 0.0
}
override fun dive() {
println("$name entering water mode")
}
override fun surface() {
println("$name returning to land mode")
}
}
fun main() {
val vehicles = listOf(
Airplane("Boeing 747", 900.0, 12000.0),
Submarine("Nuclear Sub", 50.0, 500.0),
AmphibiousVehicle("Amphibious Car", 80.0, 10.0)
)
vehicles.forEach { vehicle ->
println("=== ${vehicle.name} ===")
println("Fuel type: ${vehicle.fuelType}")
println("Max speed: ${vehicle.maxSpeed} km/h")
vehicle.start()
vehicle.accelerate(50.0)
// Execute specific operations based on type
when (vehicle) {
is Flyable -> {
vehicle.takeOff()
vehicle.fly(5000.0)
vehicle.land()
}
is Swimmable -> {
vehicle.dive()
vehicle.swim(20.0)
vehicle.surface()
}
}
vehicle.brake(30.0)
vehicle.stop()
println()
}
}Practical Application Examples
Plugin System Design
kotlin
// Plugin interface
interface Plugin {
val name: String
val version: String
val description: String
fun initialize(): Boolean
fun execute(context: PluginContext): PluginResult
fun cleanup()
// Default implementation
fun isCompatible(systemVersion: String): Boolean = true
}
// Plugin context
data class PluginContext(
val parameters: Map<String, Any>,
val workingDirectory: String,
val logger: Logger
)
// Plugin result
sealed class PluginResult {
object Success : PluginResult()
data class Error(val message: String, val exception: Throwable? = null) : PluginResult()
data class Warning(val message: String) : PluginResult()
}
// Logger interface
interface Logger {
fun info(message: String)
fun warn(message: String)
fun error(message: String, exception: Throwable? = null)
}
// Abstract plugin base class
abstract class BasePlugin(
override val name: String,
override val version: String,
override val description: String
) : Plugin {
protected var isInitialized = false
override fun initialize(): Boolean {
if (isInitialized) return true
return try {
onInitialize()
isInitialized = true
true
} catch (e: Exception) {
false
}
}
override fun cleanup() {
if (isInitialized) {
onCleanup()
isInitialized = false
}
}
protected abstract fun onInitialize()
protected abstract fun onCleanup()
protected fun validateContext(context: PluginContext, requiredParams: List<String>): Boolean {
return requiredParams.all { it in context.parameters }
}
}
// Concrete plugin implementation
class FileProcessorPlugin : BasePlugin(
name = "File Processor",
version = "1.0.0",
description = "Plugin for processing files"
) {
override fun onInitialize() {
println("Initializing file processor plugin")
}
override fun onCleanup() {
println("Cleaning up file processor plugin")
}
override fun execute(context: PluginContext): PluginResult {
if (!validateContext(context, listOf("inputFile", "outputFile"))) {
return PluginResult.Error("Missing required parameters: inputFile, outputFile")
}
val inputFile = context.parameters["inputFile"] as String
val outputFile = context.parameters["outputFile"] as String
context.logger.info("Starting file processing: $inputFile")
return try {
// Simulate file processing
Thread.sleep(100)
context.logger.info("File processing complete: $outputFile")
PluginResult.Success
} catch (e: Exception) {
context.logger.error("File processing failed", e)
PluginResult.Error("File processing failed: ${e.message}", e)
}
}
}
class DataValidatorPlugin : BasePlugin(
name = "Data Validator",
version = "2.1.0",
description = "Plugin for validating data formats"
) {
private val validationRules = mutableMapOf<String, (Any) -> Boolean>()
override fun onInitialize() {
println("Initializing data validator plugin")
// Add default validation rules
validationRules["email"] = { value ->
value is String && value.contains("@") && value.contains(".")
}
validationRules["phone"] = { value ->
value is String && value.matches(Regex("\\d{3}-\\d{3}-\\d{4}"))
}
}
override fun onCleanup() {
println("Cleaning up data validator plugin")
validationRules.clear()
}
override fun execute(context: PluginContext): PluginResult {
if (!validateContext(context, listOf("data", "rules"))) {
return PluginResult.Error("Missing required parameters: data, rules")
}
@Suppress("UNCHECKED_CAST")
val data = context.parameters["data"] as Map<String, Any>
@Suppress("UNCHECKED_CAST")
val rules = context.parameters["rules"] as List<String>
val errors = mutableListOf<String>()
for (rule in rules) {
val validator = validationRules[rule]
if (validator == null) {
errors.add("Unknown validation rule: $rule")
continue
}
val value = data[rule]
if (value == null) {
errors.add("Missing field: $rule")
} else if (!validator(value)) {
errors.add("Field validation failed: $rule")
}
}
return if (errors.isEmpty()) {
context.logger.info("Data validation passed")
PluginResult.Success
} else {
context.logger.warn("Data validation failed: ${errors.joinToString(", ")}")
PluginResult.Warning("Validation failed: ${errors.joinToString(", ")}")
}
}
}
// Plugin manager
class PluginManager {
private val plugins = mutableMapOf<String, Plugin>()
private val logger = ConsoleLogger()
fun registerPlugin(plugin: Plugin): Boolean {
return if (plugin.initialize()) {
plugins[plugin.name] = plugin
logger.info("Plugin registered: ${plugin.name} v${plugin.version}")
true
} else {
logger.error("Plugin initialization failed: ${plugin.name}")
false
}
}
fun executePlugin(pluginName: String, context: PluginContext): PluginResult? {
val plugin = plugins[pluginName]
return if (plugin != null) {
logger.info("Executing plugin: $pluginName")
plugin.execute(context)
} else {
logger.error("Plugin not found: $pluginName")
null
}
}
fun unregisterPlugin(pluginName: String) {
plugins[pluginName]?.let { plugin ->
plugin.cleanup()
plugins.remove(pluginName)
logger.info("Plugin unloaded: $pluginName")
}
}
fun listPlugins(): List<Plugin> = plugins.values.toList()
}
// Console logger implementation
class ConsoleLogger : Logger {
override fun info(message: String) {
println("[INFO] $message")
}
override fun warn(message: String) {
println("[WARN] $message")
}
override fun error(message: String, exception: Throwable?) {
println("[ERROR] $message")
exception?.printStackTrace()
}
}
fun main() {
val pluginManager = PluginManager()
val logger = ConsoleLogger()
// Register plugins
pluginManager.registerPlugin(FileProcessorPlugin())
pluginManager.registerPlugin(DataValidatorPlugin())
// List all plugins
println("Registered plugins:")
pluginManager.listPlugins().forEach { plugin ->
println("- ${plugin.name} v${plugin.version}: ${plugin.description}")
}
println()
// Execute file processor plugin
val fileContext = PluginContext(
parameters = mapOf(
"inputFile" to "input.txt",
"outputFile" to "output.txt"
),
workingDirectory = "/tmp",
logger = logger
)
val fileResult = pluginManager.executePlugin("File Processor", fileContext)
println("File processing result: $fileResult")
println()
// Execute data validator plugin
val validationContext = PluginContext(
parameters = mapOf(
"data" to mapOf(
"email" to "user@example.com",
"phone" to "123-456-7890"
),
"rules" to listOf("email", "phone")
),
workingDirectory = "/tmp",
logger = logger
)
val validationResult = pluginManager.executePlugin("Data Validator", validationContext)
println("Data validation result: $validationResult")
println()
// Unload plugins
pluginManager.unregisterPlugin("File Processor")
pluginManager.unregisterPlugin("Data Validator")
}Interface vs Abstract Class
Selection Guide
kotlin
// Scenarios for using interfaces
interface Serializable {
fun serialize(): String
fun deserialize(data: String)
}
interface Comparable<T> {
fun compareTo(other: T): Int
}
// Scenarios for using abstract classes
abstract class DatabaseConnection(val connectionString: String) {
protected var isConnected = false
// Common connection logic
fun connect(): Boolean {
if (isConnected) return true
return try {
doConnect()
isConnected = true
onConnected()
true
} catch (e: Exception) {
onConnectionError(e)
false
}
}
fun disconnect() {
if (isConnected) {
doDisconnect()
isConnected = false
onDisconnected()
}
}
// Methods subclasses must implement
protected abstract fun doConnect()
protected abstract fun doDisconnect()
// Optional hook methods
protected open fun onConnected() {}
protected open fun onDisconnected() {}
protected open fun onConnectionError(exception: Exception) {
println("Connection error: ${exception.message}")
}
}
// Concrete implementations
class MySQLConnection(connectionString: String) : DatabaseConnection(connectionString) {
override fun doConnect() {
println("Connecting to MySQL database: $connectionString")
}
override fun doDisconnect() {
println("Disconnecting from MySQL")
}
override fun onConnected() {
println("MySQL connection established")
}
}
class PostgreSQLConnection(connectionString: String) : DatabaseConnection(connectionString) {
override fun doConnect() {
println("Connecting to PostgreSQL database: $connectionString")
}
override fun doDisconnect() {
println("Disconnecting from PostgreSQL")
}
}Best Practices
1. Interface Design Principles
kotlin
// Good interface design: single responsibility
interface UserRepository {
fun findById(id: String): User?
fun save(user: User): User
fun delete(id: String): Boolean
}
interface UserValidator {
fun validate(user: User): ValidationResult
}
// Avoid: overly large interfaces
interface BadUserService {
// Data access
fun findUser(id: String): User?
fun saveUser(user: User): User
// Validation
fun validateUser(user: User): Boolean
// Email sending
fun sendWelcomeEmail(user: User)
// Logging
fun logUserAction(action: String)
}2. Using Default Implementations Wisely
kotlin
interface EventListener<T> {
fun onEvent(event: T)
// Provide default empty implementations
fun onError(error: Throwable) {
// Default: don't handle errors
}
fun onComplete() {
// Default: don't handle completion event
}
}
// Users only need to implement methods they care about
class UserEventListener : EventListener<String> {
override fun onEvent(event: String) {
println("User event: $event")
}
// No need to implement onError and onComplete
}3. Composition Over Inheritance
kotlin
// Use composition and interfaces instead of deep inheritance
interface Engine {
fun start()
fun stop()
val horsepower: Int
}
interface Transmission {
fun shiftGear(gear: Int)
val currentGear: Int
}
class Car(
private val engine: Engine,
private val transmission: Transmission
) {
fun start() {
engine.start()
}
fun drive() {
if (transmission.currentGear == 0) {
transmission.shiftGear(1)
}
println("Car driving with ${engine.horsepower} horsepower")
}
}Next Steps
After mastering interfaces and abstract classes, let's learn about exception handling in Kotlin.
Next Chapter: Exception Handling
Exercises
- Design a graphics drawing system using interfaces to define behaviors of different shapes
- Create a payment system that supports multiple payment methods (credit card, PayPal, etc.)
- Implement a message queue system that supports different message processing strategies
- Design a game character system using abstract classes and interfaces to implement different classes and skills
- Create a data access layer that supports operations on multiple database types