11
Classes
Written by Victoria Gonda
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
In this chapter, you’ll get acquainted with classes, which are named types. Classes are one of the cornerstones of object-oriented programming, a style of programming where the types have both data and behavior. In classes, data takes the form of properties and behavior is implemented using functions called methods.
Open the chapter starter project to start learning!
Creating classes
Consider the following class definition in Kotlin and add it to your Kotlin file, outside of any main()
function:
class Person(var firstName: String, var lastName: String) {
val fullName
get() = "$firstName $lastName"
}
That’s simple enough! The keyword class
is followed by the name of the class, Person
. Inside the parentheses after the class name is the primary constructor for the class, and for Person
you’re indicating that there are two mutable String
properties, firstName
and lastName
. You’ll see how to create other constructors in Chapter 15, “Advanced Classes.” Everything in the curly braces is a member of the class.
Note: You can place classes in a file alongside other constructs, or in their own file. For this exercise, you’ll put everything in the same file.
You create an instance of a class by using the class name and passing in arguments to the constructor. Add this to your main()
function:
val john = Person(firstName = "Johnny", lastName = "Appleseed")
The class instances are the objects of object-oriented programming, not to be confused with the Kotlin object
keyword.
Person
has another property named fullName
with a custom getter that uses the other properties in its definition:
println(john.fullName) // > Johnny Appleseed
It uses both the firstName
and lastName
properties to compute the fullName
, “Johnny Appleseed”.
Reference types
In Kotlin, an instance of a class is a mutable object. Classes are reference types. This means a variable of a class type does not store an actual instance, but a reference to a location in memory that stores the instance.
class SimplePerson(val name: String)
var var1 = SimplePerson(name = "John")
var var2 = var1
The heap vs. the stack
When you create a reference type such as a class, the system stores the actual instance in a region of memory known as the heap. References to the class instances are stored in a region of memory called the stack, unless the reference is part of a class instance, in which case the reference is stored on the heap with the rest of the class instance.
Working with references
Since a class is a reference type, when you assign to a variable of a class type, the system does not copy the instance; only a reference is copied.
var homeOwner = john
john.firstName = "John"
println(john.firstName) // > John
println(homeOwner.firstName) // > John
Mini-exercise
Change the value of lastName
on homeOwner
, then try reading fullName
on both john
and homeOwner
. What do you observe?
Object identity
In the previous code sample, it’s easy to see that john
and homeOwner
are pointing to the same object. The code is short and both references are named variables. What if you want to see if the value behind a variable is John?
println(homeOwner === john) // > true
val impostorJohn = Person(firstName = "John", lastName = "Appleseed")
john === homeOwner // true
john === impostorJohn // false
impostorJohn === homeOwner // false
// Assignment of existing variables changes the instances the variables reference.
homeOwner = impostorJohn
john === homeOwner // false
homeOwner = john
john === homeOwner // true
// Create fake, imposter Johns.
var imposters = (0..100).map {
Person(firstName = "John", lastName = "Appleseed")
}
// Equality (==) is not effective when John cannot be identified by his name alone
imposters.map {
it.firstName == "John" && it.lastName == "Appleseed"
}.contains(true) // true
// Check to ensure the real John is not found among the imposters.
println(imposters.contains(john)) // > false
// Now hide the "real" John somewhere among the imposters.
val mutableImposters = mutableListOf<Person>()
mutableImposters.addAll(imposters)
mutableImposters.contains(john) // false
mutableImposters.add(Random().nextInt(5), john)
// John can now be found among the imposters.
println(mutableImposters.contains(john)) // > true
// Since `Person` is a reference type, you can use === to grab the real John out of the list of imposters and modify the value.
// The original `john` variable will print the new last name!
val indexOfJohn = mutableImposters.indexOf(john)
if (indexOfJohn != -1) {
mutableImposters[indexOfJohn].lastName = "Bananapeel"
}
println(john.fullName) // > John Bananapeel
Mini-exercise
Write a function memberOf(person: Person, group: List<Person>): Bool
that will return true
if person
can be found inside group
, and false
if it can not.
Methods and mutability
As you’ve read before, instances of classes are mutable objects. Create the classes Student
and Grade
as defined below:
class Grade(
val letter: String,
val points: Double,
val credits: Double
)
class Student(
val firstName: String,
val lastName: String,
val grades: MutableList<Grade> = mutableListOf(),
var credits: Double = 0.0
) {
fun recordGrade(grade: Grade) {
grades.add(grade)
credits += grade.credits
}
}
val jane = Student(firstName = "Jane", lastName = "Appleseed")
val history = Grade(letter = "B", points = 9.0, credits = 3.0)
var math = Grade(letter = "A", points = 16.0, credits = 4.0)
jane.recordGrade(history)
jane.recordGrade(math)
Mutability and constants
The previous example may have had you wondering how you were able to modify jane
even though it was defined as a constant val
.
// Error: jane is a `val` constant
jane = Student(firstName = "John", lastName = "Appleseed")
var jane = Student(firstName = "Jane", lastName = "Appleseed")
jane = Student(firstName = "John", lastName = "Appleseed")
Mini-exercise
Add a property with a custom getter to Student
that returns the student’s Grade Point Average, or GPA. A GPA is defined as the number of points earned divided by the number of credits taken. For the example above, Jane earned (9 + 16 = 25) points while taking (3 + 4 = 7) credits, making her GPA (25 / 7 = 3.57).
Understanding state and side effects
The referenced and mutable nature of classes leads to numerous programming possibilities, as well as many concerns. If you update a class instance with a new value, then every reference to that instance will also see the new value.
var credits = 0.0
fun recordGrade(grade: Grade) {
grades.add(grade)
credits += grade.credits
}
println(jane.credits) // 7
// The teacher made a mistake; math has 5 credits
math = Grade(letter = "A", points = 20.0, credits = 5.0)
jane.recordGrade(math)
println(jane.credits) // 12, not 8!
Data classes
Suppose you want to define a Student
class and have added functionality, such as the ability to compare whether two students are equal in value or the ability to easily print the student data. You might define the class as follows:
class Student(
var firstName: String,
var lastName: String,
var id: Int
) {
override fun hashCode(): Int {
val prime = 31
var result = 1
result = prime * result + firstName.hashCode()
result = prime * result + id
result = prime * result + lastName.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
if (this === other)
return true
if (other == null)
return false
if (javaClass != other.javaClass)
return false
val obj = other as Student?
if (firstName != obj?.firstName)
return false
if (id != obj.id)
return false
if (lastName != obj.lastName)
return false
return true
}
override fun toString(): String {
return "Student (firstName=$firstName, lastName=$lastName, id=$id)"
}
fun copy(
firstName: String = this.firstName,
lastName: String = this.lastName,
id: Int = this.id
) = Student(firstName, lastName, id)
}
val albert = Student(
firstName = "Albert",
lastName = "Einstein",
id = 1
)
val richard = Student(
firstName = "Richard",
lastName = "Feynman",
id = 2
)
val albertCopy = albert.copy()
println(albert)
// > Student (firstName=Albert, lastName=Einstein, id=1)
println(richard)
// > Student (firstName=Richard, lastName=Feynman, id=2)
println(albert == richard) // > false
println(albert == albertCopy) // > true
println(albert === albertCopy) // > false
data class StudentData(
var firstName: String,
var lastName: String,
var id: Int
)
val marie = StudentData("Marie", "Curie", id = 1)
val emmy = StudentData("Emmy", "Noether", id = 2)
val marieCopy = marie.copy()
println(marie)
// > StudentData(firstName=Marie, lastName=Curie, id=1)
println(emmy)
// > StudentData(firstName=Emmy, lastName=Noether, id=2)
println(marie == emmy) // > false
println(marie == marieCopy) // > true
println(marie === marieCopy) // > false
Destructuring declarations
You can extract the data inside of a data class using a destructuring declaration. Just assign a variable to each of the properties of the data class in one assignment statement:
val (firstName, lastName, id) = marie
println(firstName) // > Marie
println(lastName) // > Curie
println(id) // > 1
Challenges
Here are some challenges for you to practice your new knowledge. If you get stuck at any point, check out the solutions in the materials for this chapter.
Challenge 1: Movie lists
Imagine you’re writing a movie-viewing application in Kotlin. Users can create lists of movies and share those lists with other users.
Challenge 2: T-Shirt store — data classes
Your challenge here is to build a set of objects to support a T-shirt store. Decide if each object should be a class or a data class, and go ahead and implement them all.
Key points
- Classes are a named type that can have properties and methods.
- Classes use references that are shared on assignment.
- Class instances are called objects.
- Objects are mutable.
- Mutability introduces state, which adds complexity when managing your objects.
- Data classes allow you to create simple model objects that avoid a lot of boilerplate for comparing, printing, and copying objects.
- Destructuring declarations allow you to easily extract multiple properties of data class objects.
Where to go from here?
You’ve just scratched the surface of the power and usage of classes!