The book “Head First. Kotlin »

image Hello, habrozhiteli! We have a book published to study Kotlin using the Head First technique, which goes beyond the syntax and instructions for solving specific problems. This book will give you everything you need - from the basics of the language to advanced methods. And you can practice object-oriented and functional programming.



Below the cut is an excerpt "Data Classes"



Work with data



No one wants to waste time and redo what has already been done. Most applications use classes for storing data. To simplify the work, the creators of Kotlin proposed the concept of a data class. In this chapter, you will learn how data classes help you write more elegant and concise code that you could only dream of before. We will look at the helper functions of data classes and learn how to decompose a data object into components. At the same time, we will tell you how the default parameter values ​​make the code more flexible, and also introduce you to Any, the ancestor of all superclasses.



The == operator calls a function called equals



As you already know, the == operator can be used to verify equality. Each time the == statement is executed, a function called equals is called. Each object contains an equals function, and the implementation of this function determines the behavior of the == operator.



By default, the equals function for checking equality checks whether two variable references to the same object.



To understand how it works, imagine two Wolf variables named w1 and w2. If w1 and w2 contain references to one Wolf object, when comparing them with the == operator, the result is true:



image






But if w1 and w2 contain references to different Wolf objects, comparing them with the == operator gives the result false, even if the objects contain the same property values.



image






As stated earlier, the equals function is automatically included in every object that you create. But where does this function come from?



equals inherits from the superclass Any



Each object contains a function called equals because its class inherits a function from a class named Any. The Any class is the ancestor of all classes: the resulting superclass of everything. Each class that you define is a subclass of Any, and you do not need to point this out in the program. Thus, if you write a class code called myClass, which looks like this:



class MyClass { ... }
      
      





The compiler will automatically convert it to the following form:

image






Each class is a subclass of Any and inherits its behavior. Each class is a subclass of Any, and you do not have to report this in the program.


The Importance of Any Inheritance



Including Any as the resulting superclass has two important advantages:





 val myArray = arrayOf(Car(), Guitar(), Giraffe())
      
      





The compiler notices that each object in the array has a common prototype of Any, and therefore creates an array of type Array.



The general behavior inherited by the Any class is worth a closer look.



Common behavior inherited from Any



The Any class defines several functions inherited by each class. Here are examples of basic functions and their behavior:





 val w1 = Wolf() val w1 = Wolf() val w2 = Wolf() val w2 = w1 println(w1.equals(w2)) println(w1.equals(w2)) false (equals  false, true (equals  true,   w1  w2   w1  w2        .)      —   ,   w1 == w2.
      
      







 val w = Wolf() println(w.hashCode())
      
      





523429237 (Value of the hash code w)





 val w = Wolf() println(w.toString())
      
      





Wolf @ 1f32e575



By default, the equals function checks if two objects are the same actual object.



The equals function determines the behavior of the == operator.


The Any class provides a default implementation for all the functions listed, and these implementations are inherited by all classes. However, you can override these implementations to change the default behavior of all the listed functions.



Simple equivalence check of two objects



In some situations, you need to change the implementation of the equals function to change the behavior of the == operator.



Suppose you have a Recipe class that allows you to create objects for storing recipes. In this situation, you are likely to consider two Recipe objects equal (or equivalent) if they contain a description of the same recipe. Let's say the Recipe class is defined with two properties - title and isVegetarian:



 class Recipe(val title: String, val isVegetarian: Boolean) { }
      
      





The == operator will return true if it is used to compare two Recipe objects with the same properties, title and isVegetarian:



 val r1 = Recipe("Chicken Bhuna", false) val r2 = Recipe("Chicken Bhuna", false)
      
      





image






Although you can change the behavior of the == operator by writing additional code to override the equals function, Kotlin developers have provided a more convenient solution: they created the concept of a data class. Let's see what these classes are and how they are created.



The data class allows you to create data objects.



A data class is a class for creating objects for storing data. It includes tools useful when working with data — for example, a new implementation of the equals function, which checks whether two data objects contain the same property values. If two objects contain the same data, then they can be considered equal.



To define a data class, precede the usual data definition with the data keyword. The following code converts the Recipe class created earlier into a data class:



 data class Recipe(val title: String, val isVegetarian: Boolean) { }
      
      





The data prefix converts a regular class to a data class.



How to create objects based on a data class



Data class objects are created in the same way as objects of ordinary classes: by calling the constructor of this class. For example, the following code creates a new Recipe data object and assigns it to a new variable named r1:



 val r1 = Recipe("Chicken Bhuna", false)
      
      





Data classes automatically override their equals functions to change the behavior of the == operator so that equality of objects is checked based on the property values ​​of each object. If, for example, you create two Recipe objects with the same property values, comparing the two objects with the == operator will give the result true, because they store the same data:



 val r1 = Recipe("Chicken Bhuna", false) val r2 = Recipe("Chicken Bhuna", false) //r1 == r2  true
      
      





r1 and r2 are considered “equal” because two Recipe objects contain the same data.



In addition to the new implementation of the equals function inherited from the Any superclass, data classes

also override the hashCode and toString functions. Let's see how these functions are implemented.



Class objects redefine their inherited behavior



To work with data, the data class needs objects, so it automatically provides the following implementations for the equals, hashCode, and toString functions inherited from the Any superclass:



The equals function compares property values



When defining a data class, its equals function (and therefore the == operator) still returns true if the links point to the same object. But it also returns true if the objects have the same property values ​​defined in the constructor:



 val r1 = Recipe("Chicken Bhuna", false) val r2 = Recipe("Chicken Bhuna", false) println(r1.equals(r2)) true
      
      





Data objects are considered equal if their properties contain the same value.


For equal objects, the same hashCode values ​​are returned



If two data objects are considered equal (in other words, they have the same property values), the hashCode function returns the same value for these objects:



 val r1 = Recipe("Chicken Bhuna", false) val r2 = Recipe("Chicken Bhuna", false) println(r1.hashCode()) println(r2.hashCode())
      
      





241131113

241131113



toString returns the values ​​of all properties



Finally, the toString function no longer returns the name of the class, followed by a number, but returns a useful string with the values ​​of all properties defined in the constructor of the data class:



 val r1 = Recipe("Chicken Bhuna", false) println(r1.toString()) Recipe(title=Chicken Bhuna, isVegetarian=false)
      
      





In addition to overriding functions inherited from the Any superclass, the data class also provides additional tools that provide more efficient work with data, for example, the ability to copy data objects. Let's see how these tools work.



Copying data objects using copy



If you need to create a copy of a data object by changing some of its properties, but leave the other properties in their original state, use the copy function. For this, the function is called for the object to be copied, and the names of all mutable properties with new values ​​are passed to it.



Suppose you have a Recipe object named r1, which is defined in the code like this:



 val r1 = Recipe("Thai Curry", false)
      
      





image






If you want to create a copy of the Recipe object, replacing the value of the isVegetarian property with true, this is done like this:



image






In essence, this means "create a copy of r1, change the value of its isVegetarian property to true, and assign a new object to a variable named r2." This creates a new copy of the object, and the original object remains unchanged.



In addition to the copy function, data classes also provide a set of functions for splitting a data object into a set of values ​​of its properties - this process is called destructuring. Let's see how this is done.



Data classes define function componentN ...



When defining a data class, the compiler automatically adds to the class a set of functions that can be used as an alternative mechanism for accessing object property values. These functions are known under the general name of the componentN functions, where N is the number of properties to be extracted (in the declaration order).



To see how componentN functions work, suppose you have the following Recipe object:



 val r = Recipe("Chicken Bhuna", false)
      
      





If you want to get the value of the first property of the object (title property), you can call the component1 () function of the object for this:



 val title = r.component1()
      
      





component1 () returns the reference that is contained in the first property defined in the constructor of the data class.



The function does the same as the following code:



 val title = r.title
      
      





The code with the function is more universal. Why are the ComponentN functions so useful in data classes?



... designed to restructure data objects



The generic componentN functions are useful primarily because they provide a simple and convenient way to split a data object into property values, or to destruct it.



Suppose you want to take the values ​​of the properties of a Recipe object and assign the value of each of its properties to a separate variable. Instead of code



 val title = r.title val vegetarian = r.isVegetarian
      
      





with sequential processing of each property, you can use the following code:



 val (title, vegetarian) = r
      
      





Assigns title to the first property r and vegetarian to the second property.



This code means "create two variables, title and vegetarian, and assign the value of one of the r properties of each variable." He does the same as the next fragment



 val title = r.component1() val vegetarian = r.component2()
      
      





but it turns out more compact.



The === operator always checks whether two variables refer to the same object.



If you want to check whether two variables refer to the same object regardless of their type, use the === operator instead of ==. The === operator gives the result true if (and only if) when two variables contain a reference to one actual object. If you have two variables, x and y, and the following expression:



 x === y
      
      





gives the result true, then you know that the variables x and y must refer to the same object.



Unlike the == operator, the behavior of the === operator is independent of the equals function. The === operator always behaves the same regardless of the type of class.



Now that you’ve learned how to create and use data classes, create a project for the Recipe code.



Creating a Recipes Project



Create a new Kotlin project for the JVM and name it “Recipes”. Then create a new

Kotlin file named Recipes.kt: select the src folder, open the File menu and select the command

New → Kotlin File / Class. Enter the file name “Recipes” and select the File option in the Kind group.



We add a new data class to the project called Recipe and create Recipe data objects. Below is the code. Update your version of Recipes.kt and bring it into line with ours:



 data class Recipe(val title: String, val isVegetarian: Boolean) (  {} ,        .) fun main(args: Array<String>) { val r1 = Recipe("Thai Curry", false) val r2 = Recipe("Thai Curry", false) val r3 = r1.copy(title = "Chicken Bhuna") (  r1    title) println("r1 hash code: ${r1.hashCode()}") println("r2 hash code: ${r2.hashCode()}") println("r3 hash code: ${r3.hashCode()}") println("r1 toString: ${r1.toString()}") println("r1 == r2? ${r1 == r2}") println("r1 === r2? ${r1 === r2}") println("r1 == r3? ${r1 == r3}") val (title, vegetarian) = r1 ( r1) println("title is $title and vegetarian is $vegetarian") }
      
      





When you run your code, the following text will appear in the IDE's output window:



 r1 hash code: -135497891 r2 hash code: -135497891 r3 hash code: 241131113 r1 toString: Recipe(title=Thai Curry, isVegetarian=false) r1 == r2? true r1 === r2? false r1 == r3? false title is Thai Curry and vegetarian is false
      
      







»More details on the book can be found on the publisher’s website

» Contents

» Excerpt



25% discount on coupon for Khabrozhitel - Kotlin



Upon payment of the paper version of the book, an electronic book is sent by e-mail.



All Articles