Search

2. Introduction to Objects

ģƒģ„±ģ¼
2021/03/30 08:26
태그

2.1 Objects Everywhere

Objects store data using properties (vals and vars) and perform operations with this data using function.
Some definitions:
•
Class: Defines properties and functions for what is essentially a new data type. Classes are also calledĀ user-defined types.
•
Member: Either a property or a function of a class.
•
Member function: A function that works only with a specific class of object.
•
Creating an object: Making aĀ valĀ orĀ varĀ of a class. Also calledĀ creating an instanceĀ of that class.
Because classes defineĀ stateĀ andĀ behavior, we can even refer to instances of built-in types likeĀ DoubleĀ orĀ BooleanĀ as objects.
Consider Kotlin'sĀ IntRangeĀ class:
We create two objects (instances) of theĀ IntRangeĀ class. Each object has its own piece of storage in memory.Ā IntRangeĀ is a class, but a particular rangeĀ r1Ā from 0 to 10 is an object that is distinct from rangeĀ r2.
Numerous operations are available for anĀ IntRangeĀ object. Some are straightforward, likeĀ sum(), and others require more understanding before you can use them. If you try calling one that needs arguments, the IDE will ask for those arguments.
To learn about a particular member function, look it up in theĀ Kotlin documentation. Notice the magnifying glass icon in the top right area of the page. Click on that and typeĀ IntRangeĀ into the search box. Click onĀ kotlin.ranges > IntRangeĀ from the resulting search. You'll see the documentation for theĀ IntRangeĀ class. You can study all the member functions—theĀ Application Programming InterfaceĀ (API)—of the class. Although you won't understand most of it at this time, it's helpful to become comfortable looking things up in the Kotlin documentation.
AnĀ IntRangeĀ is a kind of object, and a defining characteristic of an object is that you perform operations on it. Instead of "performing an operation," we sayĀ calling a member function. To call a member function for an object, start with the object identifier, then a dot, then the name of the operation:
BecauseĀ sum()Ā is a member function defined forĀ IntRange, you call it by sayingĀ r.sum(). This adds up all the numbers in thatĀ IntRange.
Earlier object-oriented languages used the phrase "sending a message" to describe calling a member function for an object. Sometimes you'll still see that terminology.
Classes can have many operations (member functions). It's easy to explore classes using an IDE (integrated development environment) that includes a feature calledĀ code completion. For example, if you typeĀ .sĀ after an object identifier within IntelliJ IDEA, it shows all the members of that object that begin withĀ s:
Try using code completion on other objects. For example, you can reverse a String or convert all the characters to lower case:
You can easily convert a String to an integer and back:
Later in the book we discuss strategies to handle situations when theĀ StringĀ you want to convert doesn't represent a correct integer value.
You can also convert from one numerical type to another. To avoid confusion, conversions between number types are explicit. For example, you convert anĀ Int iĀ to aĀ LongĀ by callingĀ i.toLong(), or to aĀ DoubleĀ withĀ i.toDouble():
Well-defined classes are easy for a programmer to understand, and produce code that's easy to read.

2.2 Creating Classes

Not only can you use predefined types like IntRange and String, you can also create your own types of objects.
Indeed, creating new types comprises much of the activity in object-oriented programming. You create new types by definingĀ classes.
An object is a piece of the solution for a problem you're trying to solve. Start by thinking of objects as expressing concepts. As a first approximation, if you discover a "thing" in your problem, represent that thing as an object in your solution.
Suppose you want to create a program to manage animals in a zoo. It makes sense to categorize the different types of animals based on how they behave, their needs, animals they get along with and those they fight with. Everything different about a species of animal is captured in the classification of that animal's object. Kotlin uses theĀ classĀ keyword to create a new type of object:
To define a class, start with theĀ classĀ keyword, followed by an identifier for your new class. The class name must begin with a letter (A-Z, upper or lower case), but can include things like numbers and underscores. Following convention, we capitalize the first letter of a class name, and lowercase the first letter of allĀ vals andĀ vars.
Animals.ktĀ starts by defining three new classes, then creates four objects (also calledĀ instances) of those classes.
GiraffeĀ is a class, but a particular five-year-old male giraffe that lives in Botswana is anĀ object.Ā Each object is different from all others, so we give them names likeĀ g1Ā andĀ g2.
Notice the rather cryptic output of the last four lines. The part before theĀ @Ā is the class name, and the number after theĀ @Ā is the address where the object is located in your computer's memory. Yes, that's a number even though it includes some letters—it's calledĀ "hexadecimal notation". Every object in your program has its own unique address.
The classes defined here (Giraffe,Ā Bear, andĀ Hippo) are as simple as possible: the entire class definition is a single line. More complex classes use curly braces ({Ā andĀ }) to create aĀ class bodyĀ containing the characteristics and behaviors for that class.
A function defined within a class belongs to that class. In Kotlin, we call theseĀ member functionsĀ of the class. Some object-oriented languages like Java choose to call themĀ methods, a term that came from early object-oriented languages like Smalltalk. To emphasize the functional nature of Kotlin, the designers chose to drop the termĀ method, as some beginners found the distinction confusing. Instead, the termĀ functionĀ is used throughout the language.
If it is unambiguous, we will just say "function." If we must make the distinction:
•
MemberĀ functions belong to a class.
•
Top-levelĀ functions exist by themselves and are not part of a class.
Here,Ā bark()Ā belongs to theĀ DogĀ class:
InĀ main(), we create aĀ DogĀ object and assign it toĀ val dog. Kotlin emits a warning because we never useĀ dog.
Member functions are called (invoked) with the object name, followed by aĀ .Ā (dot/period), followed by the function name and parameter list. Here we call theĀ meow()Ā function and display the result:
A member function acts on a particular instance of a class. When you callĀ meow(), you must call it with an object. During the call,Ā meow()Ā can access other members of that object.
When calling a member function, Kotlin keeps track of the object of interest by silently passing a reference to that object. That reference is available inside the member function by using the keywordĀ this.
Member functions have special access to other elements within a class, simply by naming those elements. You can also explicitlyĀ qualifyĀ access to those elements usingĀ this. Here,Ā exercise()Ā callsĀ speak()Ā with and without qualification:
InĀ exercise(), we callĀ speak()Ā first with an explicitĀ thisĀ and then omit the qualification.
Sometimes you'll see code containing an unnecessary explicitĀ this. That kind of code often comes from programmers who know a different language whereĀ thisĀ is either required, or part of its style. Using a feature unnecessarily is confusing for the reader, who spends time trying to figure out why you're doing it. We recommend avoiding the unnecessary use ofĀ this.
Outside the class, you must sayĀ hamster.exercise()Ā andĀ hamster.speak().

2.3 Properties

A property is a var or val that's part of a class.
Defining a propertyĀ maintains stateĀ within a class. Maintaining state is the primary motivating reason for creating a class rather than just writing one or more standalone functions.
AĀ varĀ property can be reassigned, while aĀ valĀ property can't. Each object gets its own storage for properties:
Defining aĀ varĀ orĀ valĀ inside a class looks just like defining it within a function. However, theĀ varĀ orĀ valĀ becomesĀ partĀ of that class, and you must refer to it by specifying its object usingĀ dot notation, placing a dot between the object and the name of the property. You can see dot notation used for each reference toĀ percentFull.
TheĀ percentFullĀ property represents the state of the correspondingĀ CupĀ object.Ā c1.percentFullĀ andĀ c2.percentFullĀ contain different values, showing that each object has its own storage.
A member function can refer to a property within its object without using dot notation (that is, withoutĀ qualifyingĀ it):
TheĀ add()Ā member function tries to addĀ increaseĀ toĀ percentFullĀ but ensures that it doesn't go past 100%.
You must qualify both properties and member functions from outside a class.
You can define top-level properties:
Defining a top-levelĀ valĀ is safe because it cannot be modified. However, defining a mutable (var) top-level property is considered anĀ anti-pattern. As your program becomes more complicated, it becomes harder to reason correctly aboutĀ shared mutable state. If everyone in your code base can access theĀ var counter, you can't guarantee it will change correctly: whileĀ inc()Ā increasesĀ counterĀ by one, some other part of the program might decreaseĀ counterĀ by ten, producing obscure bugs. It's best to guard mutable state within a class. InĀ Constraining VisibilityĀ you'll see how to make it truly hidden.
To say thatĀ vars can be changed whileĀ vals cannot is an oversimplification. As an analogy, consider aĀ houseĀ as aĀ val, and aĀ sofaĀ inside theĀ houseĀ as aĀ var. You can modifyĀ sofaĀ because it's aĀ var. You can't reassignĀ house, though, because it's aĀ val:
AlthoughĀ houseĀ is aĀ val, its object can be modified becauseĀ sofaĀ inĀ class HouseĀ is aĀ var. DefiningĀ houseĀ as aĀ valĀ only prevents it from being reassigned to a new object.
If we make a property aĀ val, it cannot be reassigned:
Even thoughĀ sofaĀ is aĀ var, its object cannot be modified becauseĀ coverĀ inĀ class SofaĀ is aĀ val. However,Ā sofaĀ can be reassigned to a new object.
We've talked about identifiers likeĀ houseĀ andĀ sofaĀ as if they were objects. They are actuallyĀ referencesĀ to objects. One way to see this is to observe that two identifiers can refer to the same object:
WhenĀ kitchen1Ā modifiesĀ table,Ā kitchen2Ā sees the modification.Ā kitchen1.tableĀ andĀ kitchen2.tableĀ display the same output.
Remember thatĀ varĀ andĀ valĀ control references rather than objects. AĀ varĀ allows you to rebind a reference to a different object, and aĀ valprevents you from doing so.
MutabilityĀ means an object can change its state. In the examples above,Ā class HouseĀ andĀ class KitchenĀ define mutable objects whileĀ class SofaĀ defines immutable objects.

2.4 Constructors

You initialize a new object by passing information to a constructor.
Each object is an isolated world. A program is a collection of objects, so correct initialization of each individual object solves a large part of the initialization problem. Kotlin includes mechanisms to guarantee proper object initialization.
A constructor is like a special member function that initializes a new object. The simplest form of a constructor is a single-line class definition:
InĀ main(), callingĀ Wombat()Ā creates aĀ WombatĀ object. If you are coming from another object-oriented language you might expect to see aĀ newĀ keyword used here, butĀ newĀ would be redundant in Kotlin so it was omitted.
You pass information to a constructor using a parameter list, just like a function. Here, theĀ AlienĀ constructor takes a single argument:
Creating anĀ AlienĀ object requires the argument (try it without one).Ā nameĀ initializes theĀ greetingĀ property within the constructor, but it is not accessible outside the constructor—try uncommenting lineĀ [1].
If you want the constructor parameter to be accessible outside the class body, define it as aĀ varĀ orĀ valĀ in the parameter list:
These class definitions have no explicit class bodies—the bodies are implied.
WhenĀ nameĀ is defined as aĀ varĀ orĀ val, it becomes a property and is thus accessible outside the constructor.Ā valĀ constructor parameters cannot be changed, whileĀ varĀ constructor parameters are mutable.
Your class can have numerous constructor parameters:
InĀ Complex Constructors, you'll see that constructors can also contain complex initialization logic.
If an object is used when aĀ StringĀ is expected, Kotlin calls the object'sĀ toString()Ā member function. If you don't write one, you still get a defaultĀ toString():
The default toString() isn't very useful—it produces the class name and the physical address of the object (this varies from one program execution to the next). You can define your own toString():
overrideĀ is a new keyword for us. It is required here becauseĀ toString()Ā already has a definition, the one producing the primitive result.Ā overrideĀ tells Kotlin that yes, we do actually want to replace the defaultĀ toString()Ā with our own definition. The explicitness ofĀ overrideclarifies the code and prevents mistakes.
AĀ toString()Ā that displays the contents of an object in a convenient form is useful for finding and fixing programming errors. To simplify the process ofĀ debugging, IDEs provideĀ debuggersĀ that allow you to observe each step in the execution of a program and to see inside your objects.

2.5 Constraining Visibility

If you leave a piece of code for a few days or weeks, then come back to it, you might see a much better way to write it.
This is one of the prime motivations forĀ refactoring, which rewrites working code to make it more readable, understandable, and thus maintainable.
There is a tension in this desire to change and improve your code. Consumers (client programmers) require aspects of your code to be stable. You want to change it, and they want it to stay the same.
This is particularly important for libraries. Consumers of a library don't want to rewrite code for a new version of that library. However, the library creator must be free to make modifications and improvements, with the certainty that the client code won't be affected by those changes.
Therefore, a primary consideration in software design is:
Separate things that change from things that stay the same.
To control visibility, Kotlin and some other languages provideĀ access modifiers. Library creators decide what is and is not accessible by the client programmer using the modifiersĀ public,Ā private,Ā protected, andĀ internal. This atom coversĀ publicĀ andĀ private, with a brief introduction toĀ internal. We explainĀ protectedĀ later in the book.
An access modifier such asĀ privateĀ appears before the definition for a class, function, or property. An access modifier only controls access for that particular definition.
AĀ publicĀ definition is accessible by client programmers, so changes to that definition impact client code directly. If you don't provide a modifier, your definition is automaticallyĀ public, soĀ publicĀ is technically redundant. You will sometimes still specifyĀ publicĀ for the sake of clarity.
AĀ privateĀ definition is hidden and only accessible from other members of the same class. Changing, or even removing, aĀ privateĀ definition doesn't directly impact client programmers.
privateĀ classes, top-level functions, and top-level properties are accessible only inside that file:
You can access private top-level properties ([1]), classes([2]), and function([3]) from other functions and classes within RecordAnimals.kt. Kotlin prevents you from accessing a private top-level element from within another file, telling you it's private in the file:
Privacy is most commonly used for members of a class:
•
[1]Ā AĀ privateĀ property, not accessible outside the containing class.
•
[2]Ā AĀ privateĀ member function.
•
[3]Ā AĀ publicĀ member function, accessible to anyone.
•
[4]Ā No access modifier meansĀ public.
•
[5]Ā Only members of the same class can accessĀ privateĀ members.
TheĀ privateĀ keyword means no one can access that member except other members of that class. Other classes cannot accessĀ privatemembers, so it's as if you're also insulating the class against yourself and your collaborators. WithĀ private, you can freely change that member without worrying whether it affects another class in the same package. As a library designer you'll typically keep things asĀ privateĀ as possible, and expose only functions and classes to client programmers.
Any member function that is aĀ helper functionĀ for a class can be madeĀ privateĀ to ensure you don't accidentally use it elsewhere in the package and thus prohibit yourself from changing or removing that function.
The same is true for aĀ privateĀ property inside a class. Unless you must expose the underlying implementation (which is less likely than you might think), make propertiesĀ private. However, just because a reference to an object isĀ privateĀ inside a class doesn't mean some other object can't have aĀ publicĀ reference to the same object:
•
[1]Ā cĀ is now defined in the scopeĀ surroundingĀ the creation of theĀ CounterHolderĀ object on the following line.
•
[2]Ā PassingĀ cĀ as the argument to theĀ CounterHolderĀ constructor means that the newĀ CounterHolderĀ now refers to the sameĀ CounterĀ object thatĀ cĀ refers to.
•
[3]Ā TheĀ CounterĀ that is supposedlyĀ privateĀ insideĀ chĀ can still be manipulated viaĀ c.
•
[4]Ā Counter(9)Ā has no other references except withinĀ CounterHolder, so it cannot be accessed or modified by anything exceptĀ ch2.
Maintaining multiple references to a single object is calledĀ aliasingĀ and can produce surprising behavior.

Modules

Unlike the small examples in this book, real programs are often large. It can be helpful to divide such programs into one or moreĀ modules. A module is a logically independent part of a codebase. The way you divide a project into modules depends on the build system (such asĀ GradleĀ orĀ Maven) and is beyond the scope of this book.
AnĀ internalĀ definition is accessible only inside the module where it is defined.Ā internalĀ lands somewhere betweenĀ privateĀ andĀ public—use it whenĀ privateĀ is too restrictive but you don't want an element to be a part of theĀ publicĀ API. We do not useĀ internalĀ in the book's examples or exercises.
Modules are a higher-level concept. The following atom introducesĀ packages, which enable finer-grained structuring. A library is often a single module consisting of multiple packages, soĀ internalĀ elements are available within the library but are not accessible by consumers of that library.

2.6 Packages

A fundamental principle in programming is the acronym DRY: Don't Repeat Yourself.
Multiple identical pieces of code require maintenance whenever you make fixes or improvements. So duplicating code is not just extra work—every duplication creates opportunities for mistakes.
TheĀ importĀ keyword reuses code from other files. One way to useĀ importĀ is to specify a class, function or property name:
A package is an associated collection of code. Each package is usually designed to solve a particular problem, and often contains multiple functions and classes. For example, we can import mathematical constants and functions from the kotlin.math library:
Sometimes you want to use multiple third-party libraries containing classes or functions with the same name. The as keyword allows you to change names while importing:
as is useful if a library name is poorly chosen or excessively long.
You can fully qualify an import in the body of your code. In the following example, the code might be less readable due to the explicit package names, but the origin of each element is absolutely clear:
To import everything from a package, use a star:
TheĀ kotlin.mathĀ package contains a convenientĀ roundToInt()Ā that rounds theĀ DoubleĀ value to the nearest integer, unlikeĀ toInt()which simply truncates anything after a decimal point.
To reuse your code, create a package using theĀ packageĀ keyword. TheĀ packageĀ statement must be the first non-comment statement in the file.Ā packageĀ is followed by the name of your package, which by convention is all lowercase:
You can name the source-code file anything you like, unlike Java which requires the file name to be the same as the class name.
Kotlin allows you to choose any name for your package, but it's considered good style for the package name to be identical to the directory name where the package files are located (this will not always be the case for the examples in this book).
The elements in theĀ pythagoreanĀ package are now available usingĀ import:
In the remainder of this book we use package statements for any file that defines functions, classes, etc., outside of main(), to prevent name clashes with other files in the book, but we usually won't put a package statement in a file that only contains a main().

2.7 Testing

Constant testing is essential for rapid program development.
If changing one part of your code breaks other code, your tests reveal the problem right away. If you don't find out immediately, changes accumulate and you can no longer tell which change caused the problem. You'll spend aĀ lotĀ longer tracking it down.
Testing is a crucial practice, so we introduce it early and use it throughout the rest of the book. This way, you become accustomed to testing as a standard part of the programming process.
UsingĀ println()Ā to verify code correctness is a weak approach—you must scrutinize the output every time and consciously ensure that it's correct.
To simplify your experience while using this book, we created our own tiny testing system. The goal is a minimal approach that:
1.
Shows the expected result of expressions.
2.
Provides output so you know the program is running, even when all tests succeed.
3.
Ingrains the concept of testing early in your practice.
Although useful for this book, ours isĀ notĀ a testing system for the workplace. Others have toiled long and hard to create such test systems. For example:
•
JUnitĀ is one of the most popular Java test frameworks, and is easily used from within Kotlin.
•
KotestĀ is designed specifically for Kotlin, and takes advantage of Kotlin language features.
•
TheĀ Spek FrameworkĀ produces a different form of testing, calledĀ Specification Testing.
To use our testing framework, we must firstĀ importĀ it. The basic elements of the framework areĀ eqĀ (equals) andĀ neqĀ (not equals):
The code for theĀ atomictestĀ package is inĀ Appendix A: AtomicTest. We don't intend that you understand everything inĀ AtomicTest.ktĀ right now, because it uses some features that won't appear until later in the book.
To produce a clean, comfortable appearance,Ā AtomicTestĀ uses a Kotlin feature you haven't seen yet: the ability to write a function callĀ a.function(b)Ā in the text-like formĀ a function b. This is calledĀ infix notation. Only functions defined using theĀ infixĀ keyword can be called this way.Ā AtomicTest.ktĀ defines theĀ infixĀ eqĀ andĀ neqĀ used inĀ TestingExample.kt:
eqĀ andĀ neqĀ are flexible—almost anything works as a test expression. IfĀ expectedĀ is aĀ String, thenĀ expressionĀ is converted to aĀ StringĀ and the twoĀ Strings are compared. Otherwise,Ā expressionĀ andĀ expectedĀ are compared directly (without converting them first). In either case, the result ofĀ expressionĀ appears on the console so you see something when the program runs. Even when the tests succeed, you still see the result on the left ofĀ eqĀ orĀ neq. IfĀ expressionĀ andĀ expectedĀ are not equivalent,Ā AtomicTestĀ shows an error when the program runs.
The last test inĀ TestingExample.ktĀ intentionally fails so you see an example of failure output. If the two values are not equal, Kotlin displays the corresponding message starting withĀ [Error]. If you uncomment the last line and run the example above, you will see, after all the successful tests:
The actual value stored inĀ v2Ā is not what it is claimed to be in the "expected" expression.Ā AtomicTestĀ displays theĀ StringĀ representations for both expected and actual values.
eqĀ andĀ neqĀ are the basic (infix) functions defined forĀ AtomicTest—it truly is a minimal testing system. When you putĀ eqĀ andĀ neqexpressions in your examples, you'll create both a test and some console output. You verify the correctness of the program by running it.
There's a second tool inĀ AtomicTest. TheĀ traceĀ object captures output for later comparison:
Adding results toĀ traceĀ looks like a function call, so you can effectively replaceĀ println()Ā withĀ trace().
In previous atoms, we displayed output and relied on human visual inspection to catch any discrepancies. That's unreliable; even in a book where we scrutinize the code over and over, we've learned that visual inspection can't be trusted to find errors. From now on we rarely use commented output blocks becauseĀ AtomicTestĀ will do everything for us. However, sometimes we still include commented output blocks when that produces a more useful effect.
Seeing the benefits of using testing throughout the rest of the book should help you incorporate testing into your programming process. You'll probably start feeling uncomfortable when you see code that doesn't have tests. You might even decide that code without tests is broken by definition.

Testing as Part of Programming

Testing is most effective when it's built into your software development process. Writing tests ensures you get the results you expect. Many people advocate writing testsĀ beforeĀ writing the implementation code—you first make the test fail before you write the code to make it pass. This technique, calledĀ Test Driven DevelopmentĀ (TDD), is a way to ensure that you're really testing what you think you are. You'll find a more complete description of TDD on Wikipedia (search for "Test Driven Development").
There's another benefit to writing testably—it changes the way you craft your code. You could just display the results on the console. But in the test mindset you wonder, "How will I test this?" When you create a function, you decide you should return something from the function, if for no other reason than to test that result. Functions that do nothing but take input and produce output tend to generate better designs, as well.
Here's a simplified example using TDD to implement the BMI calculation fromĀ Number Types. First, we write the tests, along with an initial implementation that fails (because we haven't yet implemented the functionality):
Only the first test passes. The other tests fails and are commented. Next, we add code to determine which weights are in which categories. Now all the tests fail:
We're using Ints instead of Doubles, producing a zero result. The tests guide us to the fix:
You may choose to add additional tests for the boundary conditions.
In the exercises for this book, we include tests that your code must pass.

2.8 Exceptions

The word "exception" is used in the same sense as the phrase "I take exception to that."
An exceptional condition prevents the continuation of the current function or scope. At the point the problem occurs, you might not know what to do with it, but you cannot continue within the current context. You don't have enough information to fix the problem. So you must stop and hand the problem to another context that's able to take appropriate action.
This atom covers the basics ofĀ exceptionsĀ as an error-reporting mechanism. InĀ Section VI: Preventing Failure, we look at other ways to deal with problems.
It's important to distinguish an exceptional condition from a normal problem. A normal problem has enough information in the current context to cope with the issue. With an exceptional condition, you cannot continue processing. All you can do is leave, relegating the problem to an external context. This is what happens when youĀ throw an exception. The exception is the object that is "thrown" from the site of the error.
ConsiderĀ toInt(), which converts aĀ StringĀ to anĀ Int. What happens if you call this function for aĀ StringĀ that doesn't contain an integer value?
Uncommenting lineĀ [1]Ā produces an exception. Here, the failing line is commented so we don't stop the book's build, which checks whether each example compiles and runs as expected.
When an exception is thrown, the path of execution—the one that can't be continued—stops, and the exception object ejects from the current context. Here, it exits the context ofĀ erroneousCode()Ā and goes out to the context ofĀ main(). In this case, Kotlin only reports the error; the programmer has presumably made a mistake and must fix the code.
When an exception isn't caught, the program aborts and displays aĀ stack traceĀ containing detailed information. Uncommenting lineĀ [1]Ā inĀ ToIntException.kt, produces the following output:
The stack trace gives details such as the file and line where the exception occurred, so you can quickly discover the issue. The last two lines show the problem: in line 10 ofĀ main()Ā we callĀ erroneousCode(). Then, more precisely, in line 6 ofĀ erroneousCode()Ā we callĀ toInt().
To avoid commenting and uncommenting code to display exceptions, we use theĀ capture()Ā function from theĀ AtomicTestĀ package:
UsingĀ capture(), we compare the generated exception to the expected error message.Ā capture()Ā isn't very helpful for normal programming—it's designed specifically for this book, so you can see the exception and know that the output has been checked by the book's build system.
Another strategy when you can't successfully produce the expected result is to returnĀ null, which is a special constant denoting "no value." You can returnĀ nullĀ instead of a value of any type. Later inĀ Nullable TypesĀ we discuss the wayĀ nullĀ affects the type of the resulting expression.
The Kotlin standard library containsĀ String.toIntOrNull()Ā which performs the conversion if theĀ StringĀ contains an integer number, or producesĀ nullĀ if the conversion is impossible—nullĀ is a simple way to indicate failure:
Suppose we calculate average income over a period of months:
IfĀ monthsĀ is zero, the division inĀ averageIncome()Ā throws anĀ ArithmeticException. Unfortunately, this doesn't tell us anything about why the error occurred, what the denominator means and whether it can legally be zero in the first place. This is clearly a bug in the code—averageIncome()Ā should cope with aĀ monthsĀ ofĀ 0Ā in a way that prevents a divide-by-zero error.
Let's modifyĀ averageIncome()Ā to produce more information about the source of the problem. IfĀ monthsĀ is zero, we can't return a regular integer value as a result. One strategy is to returnĀ null:
If a function can returnĀ null, Kotlin requires that you check the result before using it (this is covered inĀ Nullable Types). Even if you only want to display output to the user, it's better to say "No full month periods have passed," rather than "Your average income for the period is: null."
Instead of executingĀ averageIncome()Ā with the wrong arguments, you can throw an exception—escape and force some other part of the program to manage the issue. YouĀ couldĀ just allow the defaultĀ ArithmeticException, but it's often more useful to throw a specific exception with a detailed error message. When, after a couple of years in production, your application suddenly throws an exception because a new feature callsĀ averageIncome()Ā without properly checking the arguments, you'll be grateful for that message:
•
[1]Ā When throwing an exception, theĀ throwĀ keyword is followed by the exception to be thrown, along with any arguments it might need. Here we use the standard exception classĀ IllegalArgumentException.
Your goal is to generate the most useful messages possible to simplify the support of your application in the future. Later you'll learn to define your own exception types and make them specific to your circumstances.

2.9 Lists

A List is a container, which is an object that holds other objects.
Containers are also calledĀ collections. When we need a basic container for the examples in this book, we normally use aĀ List.
Lists are part of the standard Kotlin package so they don't require anĀ import.
The following example creates aĀ ListĀ populated withĀ Ints by calling the standard library functionĀ listOf()Ā with initialization values:
•
[1]Ā AĀ ListĀ uses square brackets when displaying itself.
•
[2]Ā forĀ loops work well withĀ Lists:Ā for(i in ints)Ā meansĀ iĀ receives each value inĀ ints. You don't declareĀ val iĀ or give its type; Kotlin knows from the context thatĀ iĀ is aĀ forĀ loop identifier.
•
[3]Ā Square bracketsĀ indexĀ into aĀ List. AĀ ListĀ keeps its elements in initialization order, and you select them individually by number. Like most programming languages, Kotlin starts indexing at element zero, which in this case produces the valueĀ 99. Thus an index ofĀ 4produces the valueĀ 11.
Forgetting that indexing starts at zero produces the so-calledĀ off-by-oneĀ error. In a language like Kotlin we often don't select elements one at a time, but insteadĀ iterateĀ through an entire container usingĀ in. This eliminates off-by-one errors.
If you use an index beyond the last element in aĀ List, Kotlin throws anĀ ArrayIndexOutOfBoundsException:
A List can hold all different types. Here's a List of Doubles and a List of Strings:
This shows some of List's operations. Note the name "sorted" instead of "sort." When you call sorted() it produces a new List containing the same elements as the old, in sorted order-but it leaves the original List alone. Calling it "sort" implies that the original List is changed directly (a.k.a. sorted in place). Throughout Kotlin, you see this tendency of "leaving the original object alone and producing a new object." reversed() also produces a new List.

Parameterized Types

We consider it good practice to use type inference—it tends to make the cleaner and easier to read. Sometimes, however, Kotlin complains that it can't figure out what type to use, and in other cases explicitness makes the code more understandable. Here's how we tell Kotlin the type contained by a List:
Kotlin uses the initialization values to infer thatĀ numbersĀ contains aĀ ListĀ ofĀ Ints, whileĀ stringsĀ contains aĀ ListĀ ofĀ Strings.
numbers2Ā andĀ strings2Ā are explicitly-typed versions ofĀ numbersĀ andĀ strings, created by adding the type declarationsĀ List<Int>Ā andĀ List<String>. You haven't seen angle brackets before—they denote aĀ type parameter, allowing you to say, "this container holds 'parameter' objects." We pronounceĀ List<Int>Ā as "ListĀ ofĀ Int."
Type parameters are useful for components other than containers, but you often see them with container-like objects.
Return values can also have type parameters:
Kotlin infers the return type for inferred(), while explicit() specifies the function return type. You can't just say it returns a List; Kotlin will complain, so you must give the type parameter as well. When you specify the return type of a function, Kotlin enforces your intention.

Read-Only and Mutable Lists

If you don't explicitly say you want a mutableĀ List, you won't get one.Ā listOf()Ā produces a read-onlyĀ ListĀ that has no mutating functions.
If you're creating aĀ ListĀ gradually (that is, you don't have all the elements at creation time), useĀ mutableListOf(). This produces aĀ MutableListĀ that can be modified:
You can add elements to aĀ MutableListĀ usingĀ add()Ā andĀ addAll(), or the shortcutĀ +=Ā which adds a single element or another collection. BecauseĀ listĀ has no initial elements, we must tell Kotlin what type it is by providing theĀ <Int>Ā specification in the call toĀ mutableListOf().
AĀ MutableListĀ can be treated as aĀ List, in which case it cannot be changed. You can't, however, treat a read-onlyĀ ListĀ as aĀ MutableList:
Note thatĀ listĀ lacks mutation functions despite being originally created usingĀ mutableListOf()Ā insideĀ getList(). During theĀ return, the result type becomes aĀ List<Int>. The original object is still aĀ MutableList, but it is viewed through the lens of aĀ List.
AĀ ListĀ isĀ read-only—you can read its contents but not write to it. If the underlying implementation is aĀ MutableListĀ and you retain a mutable reference to that implementation, you can still modify it via that mutable reference, and any read-only references will see those changes. This is another example ofĀ aliasing, introduced inĀ Constraining Visibility:
firstĀ is an immutable reference (val) to the mutable object produced byĀ mutableListOf(1). ThenĀ secondĀ is aliased toĀ first, so it is a view of that same object.Ā secondĀ is read-only becauseĀ List<Int>Ā does not include modification functions. Note that, without the explicitĀ List<Int>Ā type declaration, Kotlin would infer thatĀ secondĀ was also a reference to a mutable object.
We're able to add an element (2) to the object becauseĀ firstĀ is a reference to a mutableĀ List. Note thatĀ secondĀ observes these changes—it cannot change theĀ ListĀ although theĀ ListĀ changes viaĀ first.

Difference of "+=" Between Mutable and Immutable

ģ“ ė¶€ė¶„ģ€ 책에 ģžˆėŠ” ė‚“ģš©ģ“ ģ•„ė‹ˆė¼ ė‚“ź°€ ģ¶”ź°€ģ ģœ¼ė”œ ģ •ė¦¬ķ•˜ź³  ģ‹¶ģ€ ė¶€ė¶„ģž„
•
리스트(List)에 ėŒ€ķ•“ += ģ—°ģ‚°ģ„ ķ•˜ėŠ” 경우, ė¦¬ģŠ¤ķŠøź°€ Mutableģ“ėƒ Immutableģ“ėƒģ— ė”°ė¼ ė™ģž‘ ė°©ģ‹ģ“ 다름
•
Mutable: 기씓 ė¦¬ģŠ¤ķŠøģ— ģš”ģ†Œ(element)넼 추가함
•
Immutable: 기씓 ė¦¬ģŠ¤ķŠøģ— ģš”ģ†Œź°€ ģ¶”ź°€ėœ 새딜욓 리스트넼 ģƒģ„±ķ•˜ģ—¬ ė°˜ķ™˜

2.10 Variable Argument Lists

The varargs keyword produces a flexibly-sized argument list.
In Lists we introduced listOf(), which takes any number of parameters and produces a List:
Using the varargs keyword, you can define a function that takes any number of arguments, just like listOf() does. varargs is short for variable argument list:
A function definition may specify only one parameter asĀ vararg. Although it's possible to specify any item in the parameter list asĀ vararg, it's usually simplest to do it for the last one.
varargĀ allows you to pass any number (including zero) of arguments. All arguments must be of the specified type.Ā varargĀ arguments are accessed using the parameter name, which becomes anĀ Array:
AlthoughĀ Arrays andĀ Lists look similar, they are implemented differently—ListĀ is a regular library class whileĀ ArrayĀ has special low-level support.Ā ArrayĀ comes from Kotlin's requirement for compatibility with other languages, especially Java.
In day-to-day programming, use aĀ ListĀ when you need a simple sequence. UseĀ Arrays only when a third-party API requires anĀ Array, or when you're dealing withĀ varargs.
In most cases you can just ignore the fact thatĀ varargĀ produces anĀ ArrayĀ and treat it as if it were aĀ List:
You can pass an Array of elements wherever a varargs is accepted. To create an Array, use arrayOf() in the same way you use listOf(). Note that an Array is always mutable. To convert an Array into a sequence of arguments (not just a single element of type Array), use the spread operator, *:
If you pass anĀ ArrayĀ of primitive types (likeĀ Int,Ā DoubleĀ orĀ Boolean) as in the example above, theĀ ArrayĀ creation function must be specifically typed. If you useĀ arrayOf(4, 5)Ā instead ofĀ intArrayOf(4, 5), lineĀ [1]Ā will produce an error complaining thatĀ inferred type is Array but IntArray was expected.
The spread operator only works with arrays. If you have aĀ ListĀ that you want to pass as a sequence of arguments, first convert it to anĀ Arrayand then apply the spread operator, as inĀ [2]. Because the result is anĀ ArrayĀ of a primitive type, we must again use the specific conversion functionĀ toIntArray().
The spread operator is especially helpful when you must passĀ varargĀ arguments to another function that also expectsĀ varargs:

Command-Line Arguments

When invoking a program on the command line, you can pass it a variable number of arguments. To capture command-line arguments, you must provide a particular parameter to main():
The parameter is traditionally calledĀ argsĀ (although you can call it anything), and the type forĀ argsĀ can only beĀ Array<String>Ā (ArrayĀ ofĀ String).
If you are using IntelliJ IDEA, you can pass program arguments by editing the corresponding "Run configuration," as shown in the last exercise for this atom.
You can also use theĀ kotlincĀ compiler to produce a command-line program. IfĀ kotlincĀ isn't on your computer, follow the instructions on theĀ Kotlin main site. Once you've entered and saved the code forĀ MainArgs.kt, type the following at a command prompt:
You provide the command-line arguments following the program invocation, like this:
You'll see this output:
If you want to turn a String parameter into a specific type, Kotlin provides conversion functions, such as a toInt() for converting to an Int, and toFloat() for converting to a Float. Using these assumes that the command-line arguments appear in a particular order. Here, the grogram expects a String, followed by something convertible to an Int, followed by something convertible to a Float:
The first line inĀ main()Ā quits the program if there aren't enough arguments. If you don't provide something convertible to anĀ IntĀ and aĀ FloatĀ as the second and third command-line arguments, you will see runtime errors (try it to see the errors).
Compile and runĀ MainArgConversion.ktĀ with the same command-line arguments we used before, and you'll see:
hamster 42 3.14159

2.11 Sets

A Set is a collection that allows only one element of each value.
The most common Set activity is to test for membership using in or cotains():
This example shows:
1.
Placing duplicate items into aĀ SetĀ automatically removes those duplicates.
2.
Element order is not important for sets. Two sets are equal if they contain the same elements.
3.
BothĀ inĀ andĀ contains()Ā test for membership.
4.
You can perform the usual Venn-diagram operations like checking for subset, union, intersection and difference, using either dot notation (set.union(other)) or infix notation (set intersect other). The functionsĀ union,Ā intersectĀ andĀ subtractĀ can be used with infix notation.
5.
Set difference can be expressed with eitherĀ subtract()Ā or the minus operator.
To remove duplicates from aĀ List, convert it to aĀ Set:
You can also useĀ distinct(), which returns aĀ List. You may callĀ toSet()Ā on aĀ StringĀ to convert it into a set of unique characters.
As withĀ List, Kotlin provides two creation functions forĀ Set. The result ofĀ setOf()Ā is read-only. To create a mutableĀ Set, useĀ mutableSetOf():
The oeprators += and -= add and remove elements to Sets, just as with Lists.

2.12 Maps

A Map connects keys to values and looks up a value when given a key.
You create a Map by providing key-value pairs to mapOf(). Using to, we separate each key from its associated value:
•
[1]Ā TheĀ []Ā operator looks up a value using a key. You can produce all the keys usingĀ keysĀ and all the values usingĀ values. CallingĀ keysĀ produces aĀ SetĀ because all keys in aĀ MapĀ must be unique, otherwise you'd have ambiguity during a lookup.
•
[2]Ā Iterating through aĀ MapĀ produces key-value pairs as map entries.
•
[3]Ā You can unpack keys and values as you iterate.
A plainĀ MapĀ is read-only. Here's aĀ MutableMap:
map[key] = valueĀ adds or changes theĀ valueĀ associated withĀ key. You can also explicitly add a pair by sayingĀ map += key to value.
mapOf()Ā andĀ mutableMapOf()Ā preserve the order in which the elements are put into theĀ Map. This is not guaranteed for other types ofĀ Map.
A read-onlyĀ MapĀ doesn't allow mutations:
The definition ofĀ mĀ creates aĀ MapĀ associatingĀ Ints withĀ Strings. If we try to replace aĀ String, Kotlin emits an error.
An expression withĀ +Ā creates a newĀ MapĀ that includes both the old elements and the new one, but doesn't affect the originalĀ Map. The only way to "add" an element to a read-onlyĀ MapĀ is by creating a newĀ Map.
AĀ MapĀ returnsĀ nullĀ if it doesn't contain an entry for a given key. If you need a result that can't beĀ null, useĀ getValue()Ā and catchĀ NoSuchElementExceptionĀ if the key is missing:
getOrDefault()Ā is usually a nicer alternative toĀ nullĀ or an exception.
You can store class instances as values in aĀ Map. Here's a map that retrieves aĀ ContactĀ using a numberĀ String:
It's possible to use class instances as keys in aĀ Map, but that's trickier so we discuss it later in the book.
Maps look like simple little databases. They are sometimes calledĀ associative arrays, because they associate keys with values. Although they are quite limited compared to a full-featured database, they are nonetheless remarkably useful (and far more efficient than a database).

2.13 Property Accessors

To read a property, use tis name. To assign a value to a mutable property, use the assignment operator =.
This reads and writes the property i:
This appears to be straightforward access to the piece of storage namedĀ i. However, Kotlin calls functions to perform the read and write operations. As you expect, the default behavior of those functions reads and writes the data stored inĀ i. In this atom you'll learn to write your ownĀ property accessorsĀ to customize the reading and writing actions.
The accessor used to get the value of a property is called aĀ getter. You create a getter by definingĀ get()Ā immediately after the property definition. The accessor used to modify a mutable property is called aĀ setter. You create a setter by definingĀ set()Ā immediately after the property definition.
The property accessors defined in the following example imitate the default implementations generated by Kotlin. We display additional information so you can see that the property accessors are indeed called during reads and writes. We indentĀ get()Ā andĀ set()Ā to visually associate them with the property, but the actual association happens becauseĀ get()Ā andĀ set()Ā are defined immediately after that property (Kotlin doesn't care about the indentation):
The definition order forĀ get()Ā andĀ set()Ā is unimportant. You can defineĀ get()Ā without definingĀ set(), and vice-versa.
The default behavior for a property returns its stored value from a getter and modifies it with a setter—the actions ofĀ [1]Ā andĀ [2]. Inside the getter and setter, the stored value is manipulated indirectly using theĀ fieldĀ keyword, which is only accessible within these two functions.
This next example uses the default implementation of the getter and adds a setter to trace changes to the propertyĀ n:
If you define a property as private, both accessors become private. you can also make the setter private and the getter public. Then you can read the property outside the class, but only change its value inside the class:
Using private set, we control the value property so it can only be incremented by one.
Normal properties store their data in a field. You can also create a property that doesn't have a field:
The properties capacity and full contain no underlying state-they are computed at the time of each access. Both capacity and full are similar to function, and you can define them as such:
In this case, using properties improves readability because capacity and fullness are properties of the case. However, don't just convert all your function to properties—first see how they read.
The Kotlin style guide prefers properties over functions when the value is cheap to calculate and the property returns the same result for each invocation as long as the object state hasn't changed.
Property accessors provide a kind of protection for properties. Many object-oriented languages rely on making a physical fieldĀ privateĀ to control access to that property. With property accessors you can add code to control or modify that access, while allowing anyone to use a property.