In Chapter 1, we learned about a variety of types in Kotlin, such as Double, String, and Boolean.
Get ready, because in this chapter, we’re going to start creating our very own types! We’ll put variables and functions together into a class.
Let’s get started!
Putting Variables and Functions Together
As you might recall, we started off this book by creating variables to hold things pertaining to a circle, such as its radius and circumference. And in Chapter 2, we created a function to calculate the circumference from the radius.
It all looked kind of scrambled, like this.
This was manageable for such a simple example, but as we come up with more and more things that we want to know about the circle, such as its diameter, area, or position, it can become quite difficult to manage all of those different variables and functions. And once we start introducing other shapes, like rectangles and triangles, it becomes even harder to keep things straight—for example, which functions give us the area of a circle, and which give us the area of a rectangle?
So in this chapter, we’ll create a new type called a Circle. This way, instead of having separate variables and functions that hold a radius and a circumference, we can have a single variable that represents the circle itself.
So instead of the scrambled variables and functions above, it’ll look more like this.
In Kotlin, we can put related variables and functions together using a class, which is a feature that we will use to create our new Circle type.
Defining a Class
Let’s create our very first class, a Circle, which will include these variables and functions.
-
The
radiusvariable. -
The read-only
pivariable. -
The
circumference()function.
Instead of jumping into all of this at once, let’s build up this class slowly—one step at a time—and look carefully at each part.
An Empty Class
First, let’s define an empty class—with no variables and no functions.
class CircleThis is all it takes to create a new class called
Circle!When we create a class, we’re creating a new type, just like
Int,Double, andString. In other words, once we define theCircleclass like this, we can use it anywhere that we’d normally put a type, such as a function parameter.fun draw(circle: Circle) { // Code that draws the circle would go here }Our First Diagram
It’s often useful to diagram our classes, so that we can visualize them and communicate our ideas to friends and coworkers. We’ll use diagrams throughout the rest of this book to help explain concepts.
So, as we’re building out this
Circleclass, we’ll also diagram it at each stage, using a diagramming standard called the Unified Modeling Language, or UML for short.We’ll start simple. To show a class, just put the name of the class in a box, as we’re doing on the left. On the right is the corresponding Kotlin code.
Now that we’ve got our new
Circletype, we could create a variable to hold it, but since our class is completely empty, it’s not particularly useful yet. So, before we do that, let’s give ourCirclearadius!Adding a
radiusPropertyIn real life, every circle has a radius, so we need to make sure that every
Circlein our code also has aradiusvariable. Here’s how we can do that.class Circle(var radius: Double)In this code, we added a new variable to the class, called
radius, which has a type ofDouble. The value ofradiuscan be changed, because of thevarthat comes before it. In Kotlin, a variable in a class like this is called a property of the class.Let’s also update our diagram to show that a
Circleclass has aradiusproperty. To do this, we draw a horizontal line under the name of the class, and write out the name of the property and its type, almost identically to how we write it in our Kotlin code.Now that we’ve got a circle class with a radius, we’re ready to start using it!
Objects
In this book, we’ve already created lots of variables. For example, here’s how we can declare and assign a variable with a
Doubletype.val radiusOfSmallCircle: Double = 5.2As mentioned, when we created the class, we made a new type called
Circle. Just like you can have a variable that’s aDoubletype, you can also have a variable that’s aCircletype. Declaring and assigning aCirclevariable is easy.val smallCircle = Circle(5.2)This creates a new variable called
smallCircle, which is assigned aCirclewith a radius of5.2.Keep in mind—just as we can have many different
Doublevalues like…
5.26.710.0… we can also have lots of different
Circlevalues like…
- A circle with a radius of
5.2.- A circle with a radius of
6.7.- A circle with a radius of
10.0.But when it comes to classes, instead of calling these values, we usually call them objects.
Constructing Objects
Let’s look at that code again.
// Declaring the class class Circle(var radius: Double) // Using the class val smallCircle = Circle(5.2)What’s happening when we write
Circle(5.2)?It’s kind of like we’re calling a function named
Circle()that has a parameter calledradiusand a return type ofCircle. These kinds of functions aren’t called functions, though. They’re called constructors because they construct a new object.Constructor Parameters
Note that when you call a constructor, you must provide an argument for every property that is listed between the constructor’s opening and closing parentheses—
(and). Since we putvar radius: Doublebetween those parentheses, we have to provide an argument of typeDoublewhen we call the constructor.Note that
radiusis actually filling two roles.
It’s a constructor parameter. We have to provide a value for it whenever we call the constructor. (However, just like with function parameters, we can also give a constructor parameter a default argument.
It’s a property. We’ll be able to get the value of the radius from any circle object. We’ll see an example of this in a moment.
When we create an object, we say that you’re creating an instance of the class. For that reason, creating an object is sometimes referred to as instantiating the class.
Classes vs. Objects
The difference between classes and objects can be confusing at first, so let’s take a moment to clarify it.
A class describes the characteristics and behavior of some concept. If we’re talking about circles, those characteristics might include its radius, diameter, circumference, and area.
An object is an actual, particular instance of that thing. Here are three circle objects.
A circle class answers questions such as these.
- “What does it mean for something to be a circle?”
- “What characteristics does it have?”
- “What does it do?”
A circle object answers questions like these.
- “What is the radius of this particular circle?”
- “What is its circumference?”
- “What is its area?”
Here are a few more examples to help distinguish between classes and objects.
- We might have a Number class, with objects like 32,768 or 6.62607015.
- We might have a Color class, with objects like red, green, and blue.
- We might have a Dog class, with objects like Fido, Rover, or Mrs. Wagglytails.
![]()
We’ll see plenty more examples of classes and objects throughout the rest of this book!
Getting a Property’s Value
Now that we’ve created a circle object, how can we get its radius?
Easy—to get the value of a property on an object, type the name of the variable, a dot, and the name of the property, like this.
val smallCircle = Circle(5.2) val radiusOfSmallCircle: Double = smallCircle.radiusThe dot character here is known as the access operator, or more casually, the dot operator.
After running that code,
radiusOfSmallCirclewill be equal to5.2. That takes care ofradius. It’s time to move onto the other property,pi.Read-Only Properties
We could put
piinside the parentheses like we did forradius, separating them with a comma.class Circle(var radius: Double, val pi: Double)But if we do this, then we’d have to provide
pievery time we instantiate a circle!val smallCircle = Circle(5.2, 3.14) val mediumCircle = Circle(6.7, 3.14) val largeCircle = Circle(10.0, 3.14)That’s not quite what we want. Since pi should always be the exact same value regardless of the particular circle, it doesn’t make sense for it to be a constructor parameter. In fact, it would be better if the calling code could never specify the value of
piwhen constructing aCircle.To do that, we can simply move
piout of the parentheses, so that it looks like this.class Circle(var radius: Double) { val pi: Double = 3.14 }Here, we added an opening brace
{and a closing brace}. Everything between those braces is called the body of the class. Inside of the body, we declarepi, and assign it a value of3.14.By moving it out of the parentheses, the
piproperty is no longer a constructor parameter, so we can continue to call the constructor with just the radius, as we did back in Listing 4.5:val smallCircle = Circle(5.2)Private Properties
As it’s currently written, we can take any
Circleobject and get the value of bothradiusandpi.val smallCircle = Circle(5.2) val radiusOfSmallCircle = smallCircle.radius val piFromSmallCircle = smallCircle.piThere aren’t too many reasons why code outside of the class would need the value of
pi. Let’s make it so that this property is only visible from inside the class. To do that, we’ll add the keywordprivatewhen we declare it.class Circle(var radius: Double) { private val pi: Double = 3.14 }Now, if we try to get the value of the
piproperty from outside of the class body, we’ll get an error.val smallCircle = Circle(5.2) val radiusOfSmallCircle = smallCircle.radius val piFromSmallCircle = smallCircle.piErrorLet’s update our UML class diagram to include the
piproperty. We can indicate the visibility of a property in the diagram.
Private properties are preceded with a
-symbol.Public properties are preceded with a
+symbol.
privateis one of a handful of keywords that can be used to tell Kotlin how “visible” we want a property or function to be. By markingpiwithprivate, we made it so that it is only visible inside the body of the class. Any code outside of the class can’t see that property.A keyword that changes the characteristics of a declared class, property, or function is called a modifier. Since the
privatemodifier controls the visibility, it’s specifically known as a visibility modifier. We’ll see other kinds of modifiers later in this book as we explore other kinds of classes and types.If we don’t use a visibility modifier, as is the case with
radius, then it’s the same thing as marking it aspublic, which means that we can see that property or function anywhere.There are two other visibility modifiers—
protectedandinternal.
We’ll look at
protectedin detail in Chapter 14, once we learn about at subclasses.We won’t cover
internalin this book, but it’s used to restrict the visibility to a single library of code.Now, we’re ready to add the
circumference()function to the class!Adding a Member Function
When a function belongs to a class, it’s often called a method or member function. Adding a method to a class is easy—simply put it into the body of the class. To start with, let’s just take the exact same function from Listing 2.3, and drop it verbatim into our
Circleclass.class Circle(var radius: Double) { private val pi: Double = 3.14 fun circumference(radius: Double) = 2 * pi * radius }When first we created the
circumference()function back in Chapter 2, it made sense for it to have a parameter namedradius. But now that we’re adding this function to the class, we can just refer to the value of theradiusproperty instead.In other words, instead of referring to the
radiusparameter, like this…… we can remove the
radiusparameter from the function, so that it refers to the property, like this.This introduces the concept of scope, which we will explore in depth in Chapter 11. For now, just be sure to remove the parameter from the
circumference()function so that2 * pi * radiuswill refer to theradiusproperty.class Circle(var radius: Double) { private val pi: Double = 3.14 fun circumference() = 2 * pi * radius }Now that
Circlehas acircumference()function, how can we call it?Calling a function on an object is done similarly to how we got the value of
radius: the name of the variable, a dot, and the name of the function.val smallCircle = Circle(5.2) val circumferenceOfSmallCircle: Double = smallCircle.circumference()Before we move on, let’s add
circumference()to our diagram!Note that the diagram does not include the body of the function. In other words,
2 * pi * radiusdoes not appear in it. That’s because class diagrams are designed to give you an idea of what data and behavior are a part of the class, without going into the specifics.Adding More Functions
When we started off this chapter, we only had a
radiusvariable and acircumference()function. Now that we’ve put those two things together into a class, it’s time to fill out ourCircleclass with other things that we might want to know about a circle.For example, we can add a function that calculates the area of the circle.
class Circle(var radius: Double) { private val pi: Double = 3.14 fun circumference() = 2 * pi * radius fun area() = pi * radius * radius } val smallCircle = Circle(5.2) val areaOfSmallCircle = smallCircle.area()We can also add a function to calculate its diameter.
class Circle(var radius: Double) { private val pi: Double = 3.14 fun circumference() = 2 * pi * radius fun area() = pi * radius * radius fun diameter() = 2 * radius } val smallCircle = Circle(5.2) val diameterOfSmallCircle = smallCircle.diameter()And, we can even call the
diameter()function from insidecircumference().class Circle(var radius: Double) { private val pi: Double = 3.14 fun circumference() = diameter() * pi fun area() = pi * radius * radius fun diameter() = 2 * radius }And now, we can add these last few functions to our UML diagram.
Anatomy of a Class
Now that we’ve covered the basics of Kotlin classes, here’s a recap of the main pieces.
There are a few important terms to know before we move on.
Properties and functions declared within a class are regarded as members of the objects created from that class.
Variables and functions that are declared within a function are said to be local to that function, because you can only use them inside that function’s body.
The term top-level is used to refer to a variable or function that is neither a member of a class nor declared within a function.
Everything is an Object
Up until this chapter, we’ve only used built-in Kotlin types, such as
Double,String, andBoolean. You might be surprised to learn that when we used those types, we were actually using classes and objects! Just like we used the dot to get properties and call functions on ourCircleobjects, we can also use a dot on any of these types. Let’s look at a few examples of member functions and properties on types that we’ve seen before.Doubles as Objects
Objects of type
Doublehave functions likeplus()andtimes(). So instead of writing ourcircumference()function like this…fun circumference() = 2 * pi * radius… we can instead write it like this.
fun circumference() = 2.times(pi).times(radius)Kotlin developers normally use the arithmetic operators (
+,-,*,/) in most cases, but the functions are there if you want them!Strings as Objects
Stringobjects also have some interesting properties and functions. Here are a few examples.
The
lengthproperty tells you how many characters (i.e., letters, numbers, and symbols) are in the string.
uppercase()will force all of the letters in the string to upper case.
drop()will remove characters from the beginning of the string.Here’s how that looks in code.
val greeting: String = "Welcome" val numberOfLettersInGreeting = greeting.length // Evaluates to 7 val loudGreeting = greeting.toUpperCase() // Evaluates to "WELCOME" val substring = greeting.drop(3) // Evaluates to just "come"Booleans as Objects
Even
Booleanvariables—which are only evertrueorfalse—are objects! For example, if you want to turn on the headlights of your car if either it’s dark or it’s raining, you can write code to do that like this.val isDark: Boolean = true val isRaining: Boolean = false val shouldTurnOnHeadlights = isDark.or(isRaining) val shouldStayHome = isDark.and(isRaining)Although it’s possible to use functions like
or()andand()on a Boolean variable, it’s usually a better idea to use the operators||and&&instead, like this:val shouldTurnOnHeadlights = isDark || isRaining // Evaluates to true val shouldStayHome = isDark && isRaining // Evaluates to falseThe fancy words for
||and&&are disjunction operator and the conjunction operator, respectively, but almost all programmers just call them “or” and “and”. The reason to favor the operators over the function calls has to do with a concept called short-circuiting, and here’s how it works.
When using
||, if the expression on the left evaluates totrue, Kotlin knows that the result of the whole thing must betrue, so it won’t bother evaluating the expression on the right.When using
&&, if the expression on the left evaluates tofalse, Kotlin knows that the result of the whole thing must befalse, so it won’t bother evaluating the expression on the right.When we’re just writing a simple case like above, where we’ve got two
Booleanvariables, this won’t make much of a difference, but if we’ve got a function call that takes a long time to calculate its result this could be a big deal.val shouldGetRaise = yearsOfService > 1 && calculateSalary() < maximumSalaryBoolean values, conjunction, and disjunction are all part of the wonderful world of Boolean algebra.
Single-Instance Objects
Let’s imagine that we’ve created classes for a few more shapes, such as triangles and rectangles. If we want a function that prints each kind, we could end up with lots of functions that aren’t grouped together in any way.
fun printCircle(circle: Circle) = println("This circle has a radius of ${circle.radius}") fun printTriangle(triangle: Triangle) = println("This triangle has an area of ${triangle.area}") fun printRectangle(rectangle: Rectangle) = println("This rectangle has a perimeter of ${rectangle.perimeter}")Much like at the beginning of this chapter, we end up with multiple functions that are related, but there’s nothing grouping them together.
Of course, we could use a class to group them, as we did with Circle back in Listing 4.19.
class ShapePrinter { fun printCircle(circle: Circle) = ... fun printTriangle(triangle: Triangle) = ... fun printRectangle(rectangle: Rectangle) = ... } val printer = ShapePrinter() val circle = Circle(5.2) printer.printCircle(circle)This certainly works, but there are some important differences between the
Circleclass and thisShapePrinterclass.For example, it made sense for a
Circleto be a class, because each instance might have a differentradiusvalue. However,ShapePrinterhas no constructor parameters. In fact, we could easily imagine just using the same, single instance ofShapePrintereverywhere throughout our code.For cases like this, instead of defining this as a class and then instantiating it into an object, we can simply declare it as an object directly.
To do this, we can use the
objectkeyword rather than theclasskeyword.object ShapePrinter { fun printCircle(circle: Circle) = ... fun printTriangle(triangle: Triangle) = ... fun printRectangle(rectangle: Rectangle) = ... } val circle = Circle(5.2) ShapePrinter.printCircle(circle)When we define an object with the
objectkeyword, there will only ever be a single instance of that object. In many programming languages, this is known as a singleton. As demonstrated in the code above, a function or property in a singleton can be called by using its type name, a dot, and the name of the member.Note that there’s no constructor for us to call here, because it’s an object rather than a class. Types declared with the
objectkeyword can have properties, but they can’t have constructor properties, because there’s no constructor for us to call!Grouping into Packages
We’ve seen how we can group properties and functions into classes and singleton objects. However, it doesn’t end here—we can group our code even further!
When we look at the contents of a computer’s hard drive, we’ll see lots of different files. To keep these files organized, they’re also separated into folders. We might even include one folder inside of another folder. This structure creates a hierarchy that keeps related files together, making it easier for us to know where to look when we’re trying to find a file.
Similarly, our Kotlin code can be broken up into separate files and folders. Whereas files and folders are a concept that applies to the computer’s operating system, Kotlin deals with the concepts of *code elements—things like variables, functions, objects, and classes—and packages.
Generally, there’s a one-to-one relationship between a project’s folders and its packages. When we spread out our Kotlin code across different folders, we should also indicate the package that corresponds to its folder. Package names are similar to the folder paths in the operating system, but instead of separating them with slashes, we separate them with dots.
To indicate the package name for a Kotlin file, use the keyword
packagefollowed by the name of the package. For example, if ourCircleclass is in a file in the/shapes/circlefolder, then at the top of the file, it should include “package shapes.circle“.package shapes.circle val pi: Double = 3.14 class Circle(var radius: Double) { fun circumference() = diameter() * pi fun area() = pi * radius * radius fun diameter() = 2 * radius }By adding this line, all of the code elements within that file will be included in the
shapes.circlepackage.One of the nice things about packages is that we can have multiple code elements with the same name in different packages.
If two code elements can have the same name, how can Kotlin distinguish between them?
Qualifying Code Elements
Until we started explicitly declaring the packages, all of our code elements were in the same default package. When an element is declared in the same package where we’re using it, we can just refer to it with its simple name.
For example, let’s say our
shapevariable is in the same package as ourmain()function. In that case, we can simply refer to it asshape.However, if we want to use a code element that’s declared in a different package, we’ll need to qualify its name to clarify exactly which one we’re talking about. For example, if we move the
main()function into theshapespackage instead of theshapes.circlepackage, we can refer to it by using its fully-qualified name, which includes its full package name, a dot, and its simple name.In this code, you might have noticed that the
shapevariable is in a deeper package within theshapespackage, where themain()function resides. In cases like this, rather than using the fully-qualified name, we can use a partially-qualified name, which looks like this.Writing out qualified names can be a lot of typing, and they can take up a lot of space in the code, so let’s look at a more common way to use code elements from other packages.
Importing Code Elements
Rather than qualifying a code element everywhere that we use it, we can import it at the beginning of the file.
For example, let’s import the
shapevariable from theshapes.circlepackage. To import a code element, simply use the keywordimportand its fully-qualified name. Imports are placed after the package declaration and before the rest of your code in the file, as seen here.package shapes import shapes.circle.shape fun main() { val circle = shape }By importing
shapefrom theshapes.circlepackage, we no longer need to qualify the name when we use it.Named Imports
In some cases, we might want to import two code elements that have the same name. For example, if we try to import
shapefrom both theshapes.circleand theshapes.rectanglepackages, we’ll get an error.In these cases, we can use a named import for one or both of the elements we’re importing, which effectively gives the element an alias in that file. For example, let’s use a named import for
shapes.circle.shape.Wildcard Imports
In bigger projects, it’s not unusual for a file to include a long list of imports. If you find yourself importing lots of elements from the same package, you can use a wildcard import instead. For example, we might need to import
Circle,shape, andpifrom theshapes.circlepackage. We could import all three of them, like this.package shapes import shapes.circle.Circle import shapes.circle.shape import shapes.circle.pi fun main() { println(Circle(1.9).radius) println(shape.radius) println(pi) }However, we can also use an asterisk
*wildcard.package shapes import shapes.circle.* fun main() { println(Circle(1.9).radius) println(shape.radius) println(pi) }Just be mindful that wildcards make it easy to import more than you expect, and could result in naming collisions that you might not have anticipated. In fact, some developers prefer never to use wildcard imports at all!
Importing from the Standard Library
Packages and imports aren’t useful just for your own code—sometimes they’re needed when using functions or types from Kotlin’s standard library. For example, the standard library includes its own definition of pi, so we don’t have to define it ourselves.
We can update our
Circleclass to use Kotlin’s pi like this.import kotlin.math.PI class Circle(var radius: Double) { fun circumference() = diameter() * PI fun area() = PI * radius * radius fun diameter() = 2 * radius }In fact, even
println()is defined in a package namedkotlin.io. However, we don’t have to importprintln()because Kotlin automatically imports everything from thekotlin.iopackage, as well as a few other packages in the standard library.Most of the code in this book is focused and self-contained such that we usually won’t need to declare packages or imports. It’s important to know about them, though, because they’re very frequently needed in real-world projects.
Summary
Classes are powerful, and even though we covered a lot of ground in this chapter, we’ve really only introduced them. They open up a whole new world of ways to represent concepts in our code. Later in this book, we’ll cover more advanced concepts related to classes, such as inheritance.
Here’s what we learned in this chapter:
- How to define a class.
- How to create an object—that is, an instance of a class.
- How to add properties to a class, and how to access them.
- How to add functions to a class, and how to call them.
- How to create a UML diagram for a single class.
- How to call functions and get properties from the built-in Kotlin types that we’ve already worked with, such as
Double,String, andBoolean.- How to create single-instance objects.
- How to group code into packages.
- How to import code elements from other packages.
Now that we’ve covered the basics of classes, it’s time to explore a special kind of class in Kotlin: an enum class. In the next chapter, we’ll see how enum classes can be used to limit our options—and why that’s a good thing!
Thanks to Tobenna Ezike and Esraa Ibrahim for reviewing this chapter.