Suppose you want to create a simulation of a zoo. You have come up with a variety of different animal species and you want to define their behaviors. You want your animals to eat, sleep, make sounds, and move around. All animals should be able to do all these things, but the exact way they do it should depend on the species of the animal.
In practice, this means that you need to create a class for each animal species and define the corresponding methods. To make the process easier and more structured, you should use abstract classes. In this topic, we will discuss what abstract classes are and how to use them in your code.
Understanding abstract classes
An abstract class is like a blueprint that can be used to create other classes. Instead of using the blueprint directly, we create new objects based on the blueprint and work with those objects.
Let's go back to the zoo example. You might have an abstract class Animal that defines common behavior for all animals, such as eating and sleeping. This class would also have abstract methods for making sounds and moving, since different animals make different sounds and move in different ways. After creating this Animal blueprint, you could use it to create subclasses for specific animals, such as Cat and Dog, which provide their own implementations of the abstract methods.
By using an abstract class in this way, you can ensure that all the subclasses have a consistent interface and share common behavior, while also allowing them to have their own unique behavior. This can make your code more organized, reusable, and easier to maintain.
In summary, an abstract class is a class that cannot be instantiated directly but serves as a blueprint for other classes. It acts as a partially implemented class, providing a common structure and behavior that subclasses can inherit and build upon.
Declaration
Abstract classes in Kotlin are declared using the abstract modifier in the class declaration.
abstract class Animal
Just like any other class, abstract classes can also have constructors. These constructors are used to initialize the class properties and can help ensure that subclasses meet certain requirements or have initial values.
abstract class Animal(val id: Int)
An abstract class can have both abstract and non-abstract members (properties and methods). To declare a member as abstract, you must use the abstract keyword explicitly. Notice that an abstract member does not have a body (an implementation) in its class.
abstract class Animal(val id: Int) {
val name: String // We get here a compile-time error: property must be initialized or be abstract
abstract fun makeSound()
fun isSleeping(): Boolean {
...
return false
}
}
In this example, the class Animal is declared as abstract using the abstract keyword. It contains a member property that doesn't have an initializer, therefore, it must be abstract or we will get a compile-time error. Additionally, there are two member functions: the first is an abstract function makeSound(), which is declared without an implementation, the second is a non-abstract function, isSleeping(), which provides a common implementation that can be inherited by subclasses.
If, after creating an abstract class, we attempt to create an object of it, we will get a compile-time error: we cannot create an instance of an abstract class.
open to being extended, and their abstract methods and properties are open to being overridden.Implementation
When a class extends an abstract class, it must provide implementations for all the abstract members declared in the abstract class.
abstract class Animal {
abstract fun move()
abstract fun makeSound()
fun eat(): Boolean = false
fun sleep(): Boolean = false
}
class Cat : Animal() {
override fun move() {
// Implementation specific to how the cat moves
}
override fun makeSound() {
// Implementation specific to what sound the cat makes
}
}
In this example, the class Cat extends the abstract class Animal. It must override and provide specific implementations for the move() and makeSound() functions declared in the Animal class. This ensures that each subclass provides its own implementation of the abstract methods.
We cannot create objects of an abstract class directly, but we can create references of abstract class types and assign objects of concrete subclasses to them. For example:
val cat: Animal = Cat()
cat.move()
cat.makeSound()Inheritance
An abstract class can also serve as a base class for other abstract classes. In such cases, subclasses are responsible for implementing any abstract methods inherited from both the superclass and its direct abstract superclass.
abstract class Animal {
abstract fun makeSound()
}
abstract class Mammal : Animal() {
abstract fun eat()
}
class Cat : Mammal() {
override fun makeSound() {
println("Meow!")
}
override fun eat() {
println("The cat is eating.")
}
}
In this example, the class Animal is an abstract class with the abstract function makeSound(). The class Mammal extends Animal and adds an additional abstract function eat(). The class Cat then extends Mammal and provides implementations for both makeSound() and eat().
By using abstract classes in this way, we can establish a hierarchy where each level provides more specialized behavior. In the above case, Mammal extends Animal to add mammal-specific behaviors, and Cat further extends Mammal to define specific behaviors for cats.
In Kotlin, it is also possible to make an abstract class inherit from an open class while overriding a non-abstract open member with an abstract one using two keywords: abstract override.
open class Polygon {
open fun draw() {
// Some default polygon drawing method
}
}
abstract class WildShape : Polygon() {
// Classes that inherit WildShape need to provide their own draw method instead of using the default on Polygon
abstract override fun draw()
}Abstract classes vs. interfaces
One of the common questions in object-oriented programming is about the difference between abstract classes and interfaces. In Kotlin, both of these concepts are used to define contracts or behaviors that classes can implement or inherit from. However, there are some key differences between them, which affect how they are used and designed.
| Abstract classes | Interfaces | |
|---|---|---|
| Instantiation | They cannot be instantiated directly. They are meant to serve as a base for subclasses to inherit from. | They cannot be instantiated directly. They define a contract of methods and properties that implementing classes must adhere to. |
| Constructors | They can have constructors, including both primary and secondary constructors. Subclasses are responsible for invoking the appropriate superclass constructor. | They cannot have constructors. They only declare methods and properties without any implementation. |
| State | They can have member variables and non-abstract methods with default implementations. They can also hold state and maintain internal data. | They cannot hold state or define member variables. They are purely focused on declaring behavior. |
| Inheritance | Subclasses can extend only one abstract class. In Kotlin, class inheritance is limited to a single class, and abstract classes provide a way to establish an inheritance hierarchy. | Implementing classes can implement multiple interfaces. Kotlin supports multiple inheritance through interfaces, allowing classes to implement multiple interfaces at once. |
| Abstract and Non-Abstract Members | They can have both abstract and non-abstract methods and properties. Subclasses must provide implementations for abstract members while inheriting non-abstract members. |
They can declare abstract methods or methods that have default implementations. Both types of methods can be overridden by implementing classes. |
When deciding between abstract classes and interfaces, consider the following guidelines:
- Use abstract classes when you want to provide a default implementation or when you need to maintain internal state within the base class.
- Use interfaces when you want to define a contract of behavior that multiple unrelated classes can implement or when you need to achieve multiple inheritance.
Using abstract classes and interfaces together
In Kotlin, it's possible to use abstract classes and interfaces together to create a more flexible class hierarchy. This approach allows you to incorporate common members and define contracts through interfaces, providing a versatile and extensible structure. Concrete classes can then extend the abstract class while implementing additional interfaces as needed.
Let's examine a simple example to understand this concept:
interface Shape {
fun calculateArea(): Double
fun calculatePerimeter(): Double
}
abstract class AbstractShape : Shape {
// Common behavior or properties for shapes can be implemented here
}
class Rectangle(private val width: Double, private val height: Double) : AbstractShape() {
override fun calculateArea(): Double {
return width * height
}
override fun calculatePerimeter(): Double {
return 2 * (width + height)
}
}
class Circle(private val radius: Double) : AbstractShape() {
override fun calculateArea(): Double {
return Math.PI * radius * radius
}
override fun calculatePerimeter(): Double {
return 2 * Math.PI * radius
}
}
In this example, we have an interface called Shape with two methods: calculateArea() and calculatePerimeter(). The abstract class AbstractShape implements the Shape interface, providing a common base for different shapes. Then, we have two concrete classes, Rectangle and Circle, which extend AbstractShape and provide specific implementations of the area and perimeter calculations for their respective shapes.
By utilizing both abstract classes and interfaces, your code becomes more flexible. Abstract classes enable you to encapsulate common behavior and state, while interfaces establish contracts for implementing classes. This combination empowers you to design class hierarchies that are maintainable, extensible, and adhering to good object-oriented principles.
Best practices
There are some best practices to keep in mind when thinking about using abstract classes:
-
Use abstract classes to define a common interface and behavior. Abstract classes are a valuable tool for defining a common interface and behavior for related classes. Use them to encapsulate common functionality and provide a consistent structure for subclasses.
-
Avoid overusing abstract classes. While abstract classes can be useful, it is important not to overuse them. Only use abstract classes when there is a clear need for a common interface and behavior among related classes. Otherwise, consider using interfaces or composition instead.
-
Design for extensibility. When designing abstract classes, think about how they might be extended in the future. Make sure that the class hierarchy is flexible and can accommodate new subclasses without requiring major changes.
-
Provide clear documentation. Abstract classes can be complex, so it's important to provide clear documentation for developers who will be using or extending them. Make sure to document the purpose of the class, its methods, and any requirements or constraints on its use.
-
Consider using interfaces in combination with abstract classes. Abstract classes and interfaces can be used together to create a more flexible class hierarchy. Consider using interfaces to define contracts for behavior, while using abstract classes to provide common implementations and maintain state.
Conclusion
Let us sum up what you have learned in this topic:
- Abstract classes are declared using the
abstractkeyword. - Abstract classes cannot be instantiated directly.
- Subclasses of abstract classes must provide implementations for all abstract methods.
- Abstract classes can have non-abstract methods with a common implementation.
- Abstract classes can serve as the base for other abstract classes, creating an inheritance hierarchy.
- Abstract classes promote code reusability and enforce a consistent structure across related classes.
- Abstract classes can implement interfaces, allowing for a combination of shared behavior through inheritance and contracts defined by interfaces.
Remember also that abstract knowledge turns into concrete skills through practice. So, what are you waiting for? Let's solve some problems.