Interfaces Workbook
Ten short exercises on Kotlin interfaces — implementing them, default methods, property contracts, resolving clashes between defaults, functional interfaces, and interface inheritance.
Practice problems for Kotlin Interfaces Carry More Than Java’s Ever Could. Each takes a minute or two. Many exercises auto-check: implement the method or property and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, on JetBrains’ Kotlin server. The declaration exercises stay attempt-then-reveal: press Run to compile, then click Show answer to check yourself. Nothing here is a trick question, just direct practice of the syntax from the lesson.
declaring and implementing
1. Implement an interface
Declare interface Greeter { fun greet(name: String): String } and a class Formal that implements it.
interface Greeter {
fun greet(name: String): String
}
fun main() {
// TODO: make Formal implement Greeter and override greet
class Formal {
fun greet(name: String) = "Good evening"
}
println(Formal().greet("Ada"))
} Show answer Hide answer
interface Greeter {
fun greet(name: String): String
}
class Formal : Greeter {
override fun greet(name: String) = "Good evening, $name."
}A class adopts an interface with : — there’s no implements keyword — and override is required.
2. A default method
Add a greetAll(names: List<String>) default method to Greeter that greets each name, built on greet.
interface Greeter {
fun greet(name: String): String
// TODO: add a default greetAll(names: List<String>) built on greet
}
class Formal : Greeter {
override fun greet(name: String) = "Hi " + name
}
fun main() {
val g = Formal()
println(g.greet("Ada"))
// once greetAll exists: println(g.greetAll(listOf("Ada", "Grace")))
} Show answer Hide answer
interface Greeter {
fun greet(name: String): String
fun greetAll(names: List<String>) =
names.joinToString("\n") { greet(it) }
} 3. A property contract
Implement the derived val label on Named so it returns "Name: <name>", built on the required val name.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun label() {
Assert.assertEquals("label reads 'Name: <name>'",
"Name: Ada", User("Ada").label)
}
}
//sampleStart
interface Named {
val name: String
val label: String
get() = "" // TODO: return "Name: " + name
}
class User(override val name: String) : Named
//sampleEnd Show answer Hide answer
interface Named {
val name: String
val label: String
get() = "Name: $name"
} 4. Provide the property
Write class User(...) implementing Named by supplying name through its constructor.
interface Named {
val name: String
val label: String
get() = "Name: " + name
}
fun main() {
// TODO: write class User implementing Named, supplying name via the constructor
class User(val name: String)
println(User("Ada").name)
} Show answer Hide answer
class User(override val name: String) : Namedlabel is inherited; only name must be provided.
clashes and functional interfaces
5. Resolve a clash
interface A { fun ping() = "A" } and interface B { fun ping() = "B" }. Implement C.ping() so it returns "AB" by calling both inherited defaults.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun ping() {
Assert.assertEquals("combine both defaults into 'AB'", "AB", C().ping())
}
}
//sampleStart
interface A { fun ping() = "A" }
interface B { fun ping() = "B" }
class C : A, B {
override fun ping() =
"A" // TODO: return "AB" using super<A>.ping() + super<B>.ping()
}
//sampleEnd Show answer Hide answer
class C : A, B {
override fun ping() = super<A>.ping() + super<B>.ping()
}When two defaults clash the compiler forces you to choose, using super<Interface>.
6. A functional interface
Declare a fun interface Validator { fun isValid(s: String): Boolean } and create a validator for non-blank strings using a lambda.
fun interface Validator {
fun isValid(s: String): Boolean
}
fun main() {
// TODO: make a Validator for non-blank strings using a lambda
val notBlank = Validator { false }
println(notBlank.isValid("hi"))
println(notBlank.isValid(" "))
} Show answer Hide answer
fun interface Validator {
fun isValid(s: String): Boolean
}
val notBlank = Validator { it.isNotBlank() } inheritance and choices
7. Interface extends interfaces
Given interface Named { val name: String } and interface Aged { val age: Int }, declare interface Person that combines both and adds a default describe().
interface Named { val name: String }
interface Aged { val age: Int }
// TODO: declare interface Person combining Named and Aged with a default describe()
class Robot(override val name: String, override val age: Int) : Named, Aged
fun main() {
val r = Robot("R2", 5)
println(r.name + " is " + r.age)
} Show answer Hide answer
interface Person : Named, Aged {
fun describe() = "$name, $age"
} 8. Implement two interfaces
Declare class Robot implementing both Named and Aged (each with one property).
interface Named { val name: String }
interface Aged { val age: Int }
fun main() {
// TODO: declare class Robot implementing BOTH Named and Aged
class Robot(val name: String, val age: Int)
val r = Robot("R2", 5)
println(r.name + " is " + r.age)
} Show answer Hide answer
class Robot(override val name: String, override val age: Int) : Named, AgedA class can implement any number of interfaces.
9. Shared stored state
Implementers need to share a stored cache field. Write the base type as the kind that can hold one, with an abstract load(key: String): String.
fun main() {
// TODO: this base type must hold a shared stored 'cache' field + an abstract load(key)
// interface or abstract class?
abstract class Repository {
abstract fun load(key: String): String
}
class MemRepo : Repository() {
override fun load(key: String) = "value for " + key
}
println(MemRepo().load("a"))
} Show answer Hide answer
abstract class Repository {
protected val cache = mutableMapOf<String, String>()
abstract fun load(key: String): String
}An interface can declare a property but can’t hold a backing field, so shared stored state calls for an abstract class.
10. A default built on abstract members
Declare interface Counter with an abstract count(): Int and a default isEmpty() returning whether the count is zero.
interface Counter {
fun count(): Int
// TODO: add a default isEmpty() returning whether count() is zero
}
class Fixed(val n: Int) : Counter {
override fun count() = n
}
fun main() {
val c = Fixed(0)
println(c.count())
// once isEmpty exists: println(c.isEmpty())
} Show answer Hide answer
interface Counter {
fun count(): Int
fun isEmpty() = count() == 0
} Going deeper: clashes and SAM
12. Resolve the clash
C implements both A and B, each with a default ping(). Override ping in C to return "AB" by calling both parents.
import org.junit.Test
import org.junit.Assert
interface A { fun ping() = "A" }
interface B { fun ping() = "B" }
class Test {
@Test fun clash() {
Assert.assertEquals("AB", C().ping())
}
}
//sampleStart
class C : A, B {
// TODO: override ping() to call both parents' defaults
}
//sampleEnd Show answer Hide answer
class C : A, B {
override fun ping() = super<A>.ping() + super<B>.ping()
}When two interfaces supply the same default, Kotlin forces you to resolve it explicitly with super<Interface> — no silent winner.
13. A lambda as an implementation
Mark Validator so a lambda can stand in for an implementation, then create one that accepts non-blank strings.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun validator() {
Assert.assertTrue(notBlank().ok("hi"))
Assert.assertFalse(notBlank().ok(" "))
}
}
//sampleStart
interface Validator { fun ok(s: String): Boolean } // TODO: make this accept a lambda
fun notBlank(): Validator =
object : Validator { override fun ok(s: String) = false } // TODO: simplify to a lambda
//sampleEnd Show answer Hide answer
fun interface Validator { fun ok(s: String): Boolean }
fun notBlank(): Validator = Validator { it.isNotBlank() }fun interface (a single abstract method) lets a lambda become the implementation — Kotlin’s SAM conversion for its own types.
Back to the lesson, Kotlin Interfaces Carry More Than Java’s, or on to the next one: extension functions.
Comments