A Shape has an x and y location on the screen, you can calculate its area and perimeter, and you can use toString to convert it to a string. Let’s start writing the code for this class.
open class Shape(val x: Double, val y: Double) {
open fun calcArea() : Double {
// ??????
return 0.0
}
open fun calcPerimeter() : Double {
// ??????
return 0.0
}
override fun toString() : String {
return String.format("(%.1f, %.1f)", this.x, this.y)
}
}
There is a problem rigth away with the calcArea and calcPerimeter methods. How do we calculate the area and perimeter of a “shape”? We don’t have that sort of problem with circles and rectangles. We know the formulas for calculating their area and perimeter (AKA circumference). But a “shape” really doesn’t exist. If I tell you to draw me a shape, you’ll ask me “which one?” Shapes are an abstraction.
Here’s how we’ll solve the problem: we will use the keyword abstract on the class declaration. This tells Kotlin that we will never directly instantiate a Shape object. If anyone tries something like the following, they will get an error message.
We will also put the keyword abstract on the declarations of calcArea and calcPerimeter. And, most important, we will provide only the method header—there will be no method body. Listing A.3.2 shows the new abstract class:
abstract class Shape(val x: Double, val y: Double) {
abstract fun calcArea() : Double
abstract fun calcPerimeter() : Double
override fun toString() : String {
return String.format("(%.1f, %.1f)", this.x, this.y)
}
}
Now let’s look at the code for the Circle class, which inherits from Shape. This is not an abstract class—we know how to calculate a circle’s area and perimeter (circumference):
The class can still use the super keyword to call methods in the parent class. Because Shape never defined the body of the calcArea and calcPerimeter methods, the Circle class must override the methods and provide a method body. If we omit a definition for calcArea, we get this error:
error: class 'Circle' is not abstract and does not implement abstract base class member:
fun calcArea(): Double
class Circle(x: Double, y: Double, val radius: Double) : Shape(x, y) {
^^^^^^^^^^^^
NoteA.3.4.
Even though you cannot directly instantiate a Shape, you can use polymorphism to create, for example, a MutableList of Shape and assign concrete (non-abstract) objects such as Circle and Rectangle to its elements, as in Listing A.3.5.
One solution to this problem is to use an interface. It is somewhat like an abstract class, except that it consists of only methods and instance variables (with their associated accessors and mutators). Listing A.3.6 shows the Electrified interface:
class ElectricBicycle(frameSize: Double, nGears: Int, override val chargeCapacity: Int) : Bicycle(frameSize, nGears), Electrified {
override var currentCharge: Double = 0.0
set(value) {
field = maxOf(0.0, minOf(chargeCapacity.toDouble(), value))
}
}
Line 1 is the key here: the class implements the interface. Because this class implements Electrified, it must override the instance variables required by the interface, and it can customize the mutator as usual.
class ElectricCargoBicycle(frameSize: Double, nGears: Int, maxLoad: Double, override val chargeCapacity: Int) : CargoBicycle(frameSize, nGears, maxLoad), Electrified {
override var currentCharge: Double = 0.0
set(value) {
field = maxOf(0.0, minOf(chargeCapacity.toDouble(), value))
}
}
NoteA.3.10.
In this particular example, we could also use composition to create a Battery class and put one of those objects into the electric bicycle and electric cargo bicycle classes.