Chapters

Hide chapters

Kotlin Apprentice

Second Edition · Android 10 · Kotlin 1.3 · IDEA

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

11. Classes
Written by Cosmin Pupăză & Joe Howard

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In this chapter, you’ll get acquainted with classes, which are 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.

Creating classes

Consider the following class definition in Kotlin:

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. 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 16, “Advanced Classes.” Everything in the curly braces is a member of the class.

You create an instance of a class by using the class name and passing in arguments to the constructor:

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

Reference types

In Kotlin, an instance of a class is a mutable object. Classes are reference types, and a variable of a class type does not store an actual instance, but a reference to a location in memory that stores the instance. If you were to create a SimplePerson class instance with only a name like this:

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?

john === homeOwner // 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. Use === to see if any of these imposters are our real John.
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: [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. Consider 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
}
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)

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 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

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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now