欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Kotlin 新书介紹: 《Atomic Kotlin》

程序员文章站 2022-03-07 23:41:02
...

Kotlin 新书介紹: 《Atomic Kotlin》

Atomic Kotlin is the book and associated learning resources by Bruce Eckel and Svetlana Isakova.

September 1, 2020: The eBook is finished and published on Leanpub. The Stepik version will be available soon, and the print book should appear within the next couple of months.

Kotlin 新书介紹: 《Atomic Kotlin》

Kotlin 新书介紹: 《Atomic Kotlin》

Table of Contents#

This shows the table of contents for the completed book.

The bold & italicized atoms in the table of contents below are those included in the Leanpub Free Sample. The Stepik Free Sample includes beginning portions of all non-bold & italicized atoms.


Section I: Programming Basics#

  • Introduction

  • Why Kotlin?

  • Hello, World!

  • var & val

  • Data Types

  • Functions

  • if Expressions

  • String Templates

  • Number Types

  • Booleans

  • Repetition with while

  • Looping & Ranges

  • The in Keyword

  • Expressions & Statements

  • Summary 1

    Section II: Introduction to Objects#

  • Objects Everywhere

  • Creating Classes

  • Properties

  • Constructors

  • Constraining Visibility

  • Packages

  • Testing

  • Exceptions

  • Lists

  • Variable Argument Lists

  • Sets

  • Maps

  • Property Accessors

  • Summary 2

    Section III: Usability#

  • Extension Functions

  • Named & Default Arguments

  • Overloading

  • when Expressions

  • Enumerations

  • Data Classes

  • Destructuring Declarations

  • Nullable Types

  • Safe Calls & the Elvis Operator

  • Non-null Assertions

  • Extensions for Nullable Types

  • Introduction to Generics

  • Extension Properties

  • break & continue

    Section IV: Functional Programming#

  • Lambdas

  • The Importance of Lambdas

  • Operations on Collections

  • Member References

  • Higher-Order Functions

  • Manipulating Lists

  • Building Maps

  • Sequences

  • Local Functions

  • Folding Lists

  • Recursion

    Section V: Object-Oriented Programming#

  • Interfaces

  • Complex Constructors

  • Secondary Constructors

  • Inheritance

  • Base Class Initialization

  • Abstract Classes

  • Upcasting

  • Polymorphism

  • Composition

  • Inheritance & Extensions

  • Class Delegation

  • Downcasting

  • Sealed Classes

  • Type Checking

  • Nested Classes

  • Objects

  • Inner Classes

  • Companion Objects

    Section VI: Preventing Failure#

  • Exception Handling

  • Check Instructions

  • The Nothing Type

  • Resource Cleanup

  • Logging

  • Unit Testing

    Section VII: Power Tools#

  • Scope Functions

  • Extension Lambdas

  • Creating Generics

  • Operator Overloading

  • Using Operators

  • Property Delegation

  • Property Delegation Tools

  • Lazy Initialization

  • Late Initialization

    Appendices#

  • Appendix A: AtomicTest

  • Appendix B: Java Interoperability


Summary 1

This atom summarizes and reviews the atoms in Section I, starting atHello, World! and ending with Expressions & Statements.

If you’re an experienced programmer, this should be your first atom. Beginning programmers should read this atom and perform the exercises as a review of Section I.

If anything isn’t clear to you, study the associated atom for that topic (the sub-headings correspond to atom titles).

Hello, World!

Kotlin supports both // single-line comments, and /*-to-*/ multiline comments. A program’s entry point is the function main():

// Summary1/Hello.kt

fun main() {
  println("Hello, world!")
}
/* Output:
Hello, world!
*/

The first line of each example in this book is a comment containing the name of the atom’s subdirectory, followed by a / and the name of the file. You can find all the extracted code examples via AtomicKotlin.com.

println() is a standard library function which takes a single Stringparameter (or a parameter that can be converted to a String). println()moves the cursor to a new line after displaying its parameter, while print() leaves the cursor on the same line.

Kotlin does not require a semicolon at the end of an expression or statement. Semicolons are only necessary to separate multiple expressions or statements on a single line.

var & val, Data Types

To create an unchanging identifier, use the val keyword followed by the identifier name, a colon, and the type for that value. Then add an equals sign and the value to assign to that val:

val identifier: Type = initialization

Once a val is assigned, it cannot be reassigned.

Kotlin’s type inference can usually determine the type automatically, based on the initialization value. This produces a simpler definition:

val identifier = initialization

Both of the following are valid:

val daysInFebruary = 28
val daysInMarch: Int = 31

var (variable) definition looks the same, using var instead of val:

var identifier1 = initialization
var identifier2: Type = initialization

Unlike a val, you can modify a var, so the following is legal:

var hoursSpent = 20
hoursSpent = 25

However, the type can’t be changed, so you get an error if you say:

hoursSpent = 30.5

Kotlin infers the Int type when hoursSpent is defined, so it won’t accept the change to a floating-point value.

Functions

Functions are named subroutines:

fun functionName(arg1: Type1, arg2: Type2, ...): ReturnType {
  // Lines of code ...
  return result
}

The fun keyword is followed by the function name and the parameter list in parentheses. Each parameter must have an explicit type, because Kotlin cannot infer parameter types. The function itself has a type, defined in the same way as for a var or val (a colon followed by the type). A function’s type is the type of the returned result.

The function signature is followed by the function body contained within curly braces. The return statement provides the function’s return value.

You can use an abbreviated syntax when the function consists of a single expression:

fun functionName(arg1: Type1, arg2: Type2, ...): ReturnType = result

This form is called an expression body. Instead of an opening curly brace, use an equals sign followed by the expression. You can omit the return type because Kotlin infers it.

Here’s a function that produces the cube of its parameter, and another that adds an exclamation point to a String:

// Summary1/BasicFunctions.kt

fun cube(x: Int): Int {
  return x * x * x
}

fun bang(s: String) = s + "!"

fun main() {
  println(cube(3))
  println(bang("pop"))
}
/* Output:
27
pop!
*/

cube() has a block body with an explicit return statement. bang()’s body is a single expression that produces the function’s return value. Kotlin infers bang()s return type of String.

Booleans

For Boolean algebra, Kotlin provides operators such as:

  • ! (not) logically negates the value (turns true to false and vice-versa).

  • && (and) returns true only if both conditions are true.

  • || (or) returns true if at least one of the conditions is true.

// Summary1/Booleans.kt

fun main() {
  val opens = 9
  val closes = 20
  println("Operating hours: $opens - $closes")
  val hour = 6
  println("Current time: " + hour)

  val isOpen = hour >= opens && hour <= closes
  println("Open: " + isOpen)
  println("Not open: " + !isOpen)

  val isClosed = hour < opens || hour > closes
  println("Closed: " + isClosed)
}
/* Output:
Operating hours: 9 - 20
Current time: 6
Open: false
Not open: true
Closed: true
*/

isOpen’s initializer uses && to test whether both conditions are true. The first condition hour >= opens is false, so the result of the entire expression becomes false. The initializer for isClosed uses ||, producingtrue if at least one of the conditions is true. The expression hour < opensis true, so the whole expression is true.

if Expressions

Because if is an expression, it produces a result. This result can be assigned to a var or val. Here, you also see the use of the else keyword:

// Summary1/IfResult.kt

fun main() {
  val result = if (99 < 100) 4 else 42
  println(result)
}
/* Output:
4
*/

Either branch of an if expression can be a multiline block of code surrounded by curly braces:

// Summary1/IfExpression.kt

fun main() {
  val activity = "swimming"
  val hour = 10

  val isOpen = if (
    activity == "swimming" ||
    activity == "ice skating") {
    val opens = 9
    val closes = 20
    println("Operating hours: " +
      opens + " - " + closes)
    hour >= opens && hour <= closes
  } else {
    false
  }
  println(isOpen)
}
/* Output:
Operating hours: 9 - 20
true
*/

A value defined inside a block of code, such as opens, is not accessible outside the scope of that block. Because they are defined globally to theif expression, activity and hour are accessible inside the if expression.

The result of an if expression is the result of the last expression of the chosen branch; here, it’s hour >= opens && hour <= closes which is true.

String Templates

You can insert a value within a String using String templates. Use a $before the identifier name:

// Summary1/StrTemplates.kt

fun main() {
  val answer = 42
  println("Found $answer!")             // [1]
  val condition = true
  println(
    "${if (condition) 'a' else 'b'}")   // [2]
  println("printing a $1")              // [3]
}
/* Output:
Found 42!
a
printing a $1
*/
  • [1] $answer substitutes the value contained in answer.

  • [2] ${if(condition) 'a' else 'b'} evaluates and substitutes the result of the expression inside ${}.

  • [3] If the $ is followed by anything unrecognizable as a program identifier, nothing special happens.

Use triple-quoted Strings to store multiline text or text with special characters:

// Summary1/ThreeQuotes.kt

fun json(q: String, a: Int) = """{
  "question" : "$q",
  "answer" : $a
}"""

fun main() {
  println(json("The Ultimate", 42))
}
/* Output:
{
  "question" : "The Ultimate",
  "answer" : 42
}
*/

You don’t need to escape special characters like " within a triple-quotedString. (In a regular String you write \" to insert a double quote). As with normal Strings, you can insert an identifier or an expression using $inside a triple-quoted String.

Number Types

Kotlin provides integer types (IntLong) and floating point types (Double). A whole number constant is Int by default and Long if you append an L. A constant is Double if it contains a decimal point:

// Summary1/NumberTypes.kt

fun main() {
  val n = 1000    // Int
  val l = 1000L   // Long
  val d = 1000.0  // Double
  println("$n $l $d")
}
/* Output:
1000 1000 1000.0
*/

An Int holds values between -231 and +231-1. Integral values can overflow; for example, adding anything to Int.MAX_VALUE produces an overflow:

// Summary1/Overflow.kt

fun main() {
  println(Int.MAX_VALUE + 1)
  println(Int.MAX_VALUE + 1L)
}
/* Output:
-2147483648
2147483648
*/

In the second println() statement we append L to 1, forcing the whole expression to be of type Long, which avoids the overflow. (A Long can hold values between -2`63` and +2`63`-1).

When you divide an Int with another Int, Kotlin produces an Int result, and any remainder is truncated. So 1/2 produces 0. If a Double is involved, the Int is promoted to Double before the operation, so 1.0/2produces 0.5.

You might expect d1 in the following to produce 3.4:

// Summary1/Truncation.kt

fun main() {
  val d1: Double = 3.0 + 2 / 5
  println(d1)
  val d2: Double = 3 + 2.0 / 5
  println(d2)
}
/* Output:
3.0
3.4
*/

Because of evaluation order, it doesn’t. Kotlin first divides 2 by 5, and integer math produces 0, yielding an answer of 3.0. The same evaluation order does produce the expected result for d2. Dividing 2.0 by 5produces 0.4. The 3 is promoted to a Double because we add it to aDouble (0.4), which produces 3.4.

Understanding evaluation order helps you to decipher what a program does, both with logical operations (Boolean expressions) and with mathematical operations. If you’re unsure about evaluation order, use parentheses to force your intention. This also makes it clear to those reading your code.

Repetition with while

while loop continues as long as the controlling Boolean-expressionproduces true:

while (Boolean-expression) {
  // Code to be repeated
}

The Boolean expression is evaluated once at the beginning of the loop and again before each further iteration.

// Summary1/While.kt

fun testCondition(i: Int) = i < 100

fun main() {
  var i = 0
  while (testCondition(i)) {
    print(".")
    i += 10
  }
}
/* Output:
..........
*/

Kotlin infers Boolean as the result type for testCondition().

The short versions of assignment operators are available for all mathematical operations (+=-=*=/=%=). Kotlin also supports the increment and decrement operators ++ and --, in both prefix and postfix form.

while can be used with the do keyword:

do {
  // Code to be repeated
} while (Boolean-expression)

Rewriting While.kt:

// Summary1/DoWhile.kt

fun main() {
  var i = 0
  do {
    print(".")
    i += 10
  } while (testCondition(i))
}
/* Output:
..........
*/

The sole difference between while and do-while is that the body of thedo-while always executes at least once, even if the Boolean expression produces false the first time.

Looping & Ranges

Many programming languages index into an iterable object by stepping through integers. Kotlin’s for allows you to take elements directly from iterable objects like ranges and Strings. For example, this for selects each character in the String "Kotlin":

// Summary1/StringIteration.kt

fun main() {
  for (c in "Kotlin") {
    print("$c ")
    // c += 1 // error:
    // val cannot be reassigned
  }
}
/* Output:
K o t l i n
*/

c can’t be explicitly defined as either a var or val—Kotlin automatically makes it a val and infers its type as Char (you can provide the type explicitly, but in practice this is rarely done).

You can step through integral values using ranges:

// Summary1/RangeOfInt.kt

fun main() {
  for (i in 1..10) {
    print("$i ")
  }
}
/* Output:
1 2 3 4 5 6 7 8 9 10
*/

Creating a range with .. includes both bounds, but until excludes the top endpoint: 1 until 10 is the same as 1..9. You can specify an increment value using step1..21 step 3.

The in Keyword

The same in that provides for loop iteration also allows you to check membership in a range. !in returns true if the tested value isn’t in the range:

// Summary1/Membership.kt

fun inNumRange(n: Int) = n in 50..100

fun notLowerCase(ch: Char) = ch !in 'a'..'z'

fun main() {
  val i1 = 11
  val i2 = 100
  val c1 = 'K'
  val c2 = 'k'
  println("$i1 ${inNumRange(i1)}")
  println("$i2 ${inNumRange(i2)}")
  println("$c1 ${notLowerCase(c1)}")
  println("$c2 ${notLowerCase(c2)}")
}
/* Output:
11 false
100 true
K true
k false
*/

in can also be used to test membership in floating-point ranges, although such ranges can only be defined using .. and not until.

Expressions & Statements

The smallest useful fragment of code in most programming languages is either a statement or an expression. These have one basic difference:

  • A statement changes state

  • An expression expresses

That is, an expression produces a result, while a statement does not. Because it doesn’t return anything, a statement must change the state of its surroundings (that is, create a side effect) to do anything useful.

Almost everything in Kotlin is an expression:

val hours = 10
val minutesPerHour = 60
val minutes = hours * minutesPerHour

In each case, everything to the right of the = is an expression, which produces a result that is assigned to the identifier on the left.

Some functions, like println(), don’t seem to produce a result, but because they are still expressions, they must return something. Kotlin has a special Unit type for these:

// Summary1/UnitReturn.kt

fun main() {
  val result = println("returns Unit")
  println(result)
}
/* Output:
returns Unit
kotlin.Unit
*/

Experienced programmers should go to Summary 2 after working the exercises for this atom.

Exercises and solutions for this atom can be found at AtomicKotlin.com.

Summary 2

This atom summarizes and reviews the atoms in Section II, fromObjects Everywhere through Property Accessors.

If you’re an experienced programmer, this is your next atom afterSummary 1, and you will go through the atoms sequentially after this.

Beginning programmers should read this atom and perform the exercises as review. If any information here isn’t clear to you, go back and study the atom for that topic.

The topics appear in appropriate order for experienced programmers, which is not the same as the order of the atoms in the book. For example, we start by introducing packages and imports so we can use our minimal test framework for the rest of the atom.

Packages & Testing

Any number of reusable library components can be bundled under a single library name using the package keyword:

// Summary2/ALibrary.kt
package com.yoururl.libraryname

// Components to reuse ...
fun f() = "result"

You can put multiple components in a single file, or spread components out among multiple files under the same package name. Here we’ve defined f() as the sole component.

To make it unique, the package name conventionally begins with your reversed domain name. In this example, the domain name is yoururl.com.

In Kotlin, the package name can be independent from the directory where its contents are located. Java requires that the directory structure correspond to the fully-qualified package name: in Java, all the content for the package com.yoururl.libraryname should be located under the com/yoururl/libraryname directory. For mixed Kotlin and Java projects, Kotlin’s style guide recommends the same practice. For pure Kotlin projects, put the directory libraryname at the top level of your project’s directory structure.

An import statement brings one or more names into the current namespace:

// Summary2/UseALibrary.kt
import com.yoururl.libraryname.*

fun main() {
  val x = f()
}

The star after libraryname tells Kotlin to import all the components of a library. You can also select components individually; details are inPackages.

In the remainder of this book we use package statements for any file that defines functions, classes, etc., outside of main(), to prevent name *es with other files in the book, but we usually won’t put a packagestatement in a file that only contains a main().

An important library for this book is atomictest, our simple testing framework. atomictest is defined in Appendix A: AtomicTest, although it uses language features you will not understand at this point in the book.

After importing atomictest, you use eq (equals) and neq (not equals) almost as if they were language keywords:

// Summary2/UsingAtomicTest.kt
import atomictest.*

fun main() {
  val pi = 3.14
  val pie = "A round dessert"
  pi eq 3.14
  pie eq "A round dessert"
  pi neq pie
}
/* Output:
3.14
A round dessert
3.14
*/

The ability to use eq/neq without any dots or parentheses is called infix notation. You can call infix functions either in the regular way:pi.eq(3.14), or using infix notation: pi eq 3.14. Both eq and neq are assertions of truth that also display the result from the left side of theeq/neq statement, and an error message if the expression on the right of the eq isn’t equivalent to the left (or is equivalent, in the case of neq). This way you see verified results in the source code.

Objects Everywhere

Kotlin is a hybrid object-functional language: it supports both object-oriented and functional programming paradigms.

Objects contain vals and vars to store data (these are called properties) and perform operations using functions defined within a class, called member functions (when it’s unambiguous, we just say “functions”). A class defines properties and member functions for what is essentially a new, user-defined data type. When you create a val or var of a class, it’s called creating an object or creating an instance.

An especially useful type of object is the container, also called collection. A container is an object that holds other objects. In this book, we often use the List because it’s the most general-purpose sequence. Here we perform several operations on a List that holds Doubles. listOf() creates a new List from its arguments:

// Summary2/ListCollection.kt
import atomictest.eq

fun main() {
  val lst = listOf(19.2, 88.3, 22.1)
  lst[1] eq 88.3  // Indexing
  lst.reversed() eq listOf(22.1, 88.3, 19.2)
  lst.sorted() eq listOf(19.2, 22.1, 88.3)
  lst.max() eq 88.3
  lst.min() eq 19.2
  lst.sum() eq 129.6
}

No import statement is required to use a List.

Kotlin uses square brackets for indexing into sequences. Indexing is zero-based.

This example also shows a few of the many standard library functions available for Lists: sorted()reversed()max()min() and sum(). To understand these functions, consult the online Kotlin documentation.

When you call sorted() or reversed()lst is not modified. Instead, a newList is created and returned, containing the desired result. This approach of never modifying the original object is consistent throughout Kotlin libraries and you should endeavor to follow this pattern when writing your own code.

Creating Classes

A class definition consists of the class keyword, a name for the class, and an optional body. The body contains property definitions (vals and vars) and function definitions.

This example defines a NoBody class without a body, and classes with valproperties:

// Summary2/ClassBodies.kt
package summary2

class NoBody

class SomeBody {
  val name = "Janet Doe"
}

class EveryBody {
  val all = listOf(SomeBody(),
    SomeBody(), SomeBody())
}

fun main() {
  val nb = NoBody()
  val sb = SomeBody()
  val eb = EveryBody()
}

To create an instance of a class, put parentheses after its name, along with arguments if those are required.

Properties within class bodies can be any type. SomeBody contains a property of type String, and EveryBody’s property is a List holding SomeBody objects.

Here’s a class with member functions:

// Summary2/Temperature.kt
package summary2
import atomictest.eq

class Temperature {
  var current = 0.0
  var scale = "f"
  fun setFahrenheit(now: Double) {
    current = now
    scale = "f"
  }
  fun setCelsius(now: Double) {
    current = now
    scale = "c"
  }
  fun getFahrenheit(): Double =
    if (scale == "f")
      current
    else
      current * 9.0 / 5.0 + 32.0
  fun getCelsius(): Double =
    if (scale == "c")
      current
    else
      (current - 32.0) * 5.0 / 9.0
}

fun main() {
  val temp = Temperature()     // [1]
  temp.setFahrenheit(98.6)
  temp.getFahrenheit() eq 98.6
  temp.getCelsius() eq 37.0
  temp.setCelsius(100.0)
  temp.getFahrenheit() eq 212.0
}

These member functions are just like the top-level functions we’ve defined outside of classes, except they belong to the class and have unqualified access to the other members of the class, such as currentand scale. Member functions can also call other member functions in the same class without qualification.

  • [1] Although temp is a val, we later modify the Temperature object. The val definition prevents the reference temp from being reassigned to a new object, but it does not restrict the behavior of the object itself.

The following two classes are the foundation of a tic-tac-toe game:

// Summary2/TicTacToe.kt
package summary2
import atomictest.eq

class Cell {
  var entry = ' '                   // [1]
  fun setValue(e: Char): String =   // [2]
    if (entry == ' ' &&
      (e == 'X' || e == 'O')) {
      entry = e
      "Successful move"
    } else
      "Invalid move"
}

class Grid {
  val cells = listOf(
    listOf(Cell(), Cell(), Cell()),
    listOf(Cell(), Cell(), Cell()),
    listOf(Cell(), Cell(), Cell())
  )
  fun play(e: Char, x: Int, y: Int): String =
    if (x !in 0..2 || y !in 0..2)
      "Invalid move"
    else
      cells[x][y].setValue(e)       // [3]
}

fun main() {
  val grid = Grid()
  grid.play('X', 1, 1) eq "Successful move"
  grid.play('X', 1, 1) eq "Invalid move"
  grid.play('O', 1, 3) eq "Invalid move"
}
  • [1] The entry property in Cell is a var so it can be modified. The single quotes in the initialization produce a Char type, so all assignments to entry must also be Chars.

  • [2] setValue() tests that the Cell is available and that you’ve passed the correct character. It returns a String result to indicate success or failure.

The Grid class holds a List containing three Lists, each containing threeCells—a matrix.

  • [3] play() checks to see if the x and y arguments are within range, then indexes into the matrix, relying on the tests performed by setValue().

Constructors

Constructors create new objects. You pass information to a constructor using its parameter list, placed in parentheses directly after the class name. A constructor call thus looks like a function call, except that the initial letter of the name is capitalized (following the Kotlin style guide). The constructor returns an object of the class:

// Summary2/WildAnimals.kt
package summary2
import atomictest.eq

class Badger(id: String, years: Int) {
  val name = id
  val age = years
  override fun toString(): String {
    return "Badger: $name, age: $age"
  }
}

class Snake(
  var type: String,
  var length: Double
) {
  override fun toString(): String {
    return "Snake: $type, length: $length"
  }
}

class Moose(
  val age: Int,
  val height: Double
) {
  override fun toString(): String {
    return "Moose, age: $age, height: $height"
  }
}

fun main() {
  Badger("Bob", 11) eq "Badger: Bob, age: 11"
  Snake("Garden", 2.4) eq
    "Snake: Garden, length: 2.4"
  Moose(16, 7.2) eq
    "Moose, age: 16, height: 7.2"
}

The parameters id and years in Badger are only available in theconstructor body. The constructor body consists of the lines of code other than function definitions; in this case, the definitions for name and age.

Often you want the constructor parameters to be available in parts of the class other than the constructor body, but without the trouble of explicitly defining new identifiers as we did with name and age. If you define your parameters as vars or vals, they becomes properties and are accessible everywhere in the class. Both Snake and Moose use this approach, and you can see that the constructor parameters are now available inside their respective toString() functions.

Constructor parameters declared with val cannot be changed, but those declared with var can.

Whenever you use an object in a situation that expects a String, Kotlin produces a String representation of that object by calling its toString()member function. To define a toString(), you must understand a new keyword: override. This is necessary (Kotlin insists on it) because toString() is already defined. override tells Kotlin that we do actually want to replace the default toString() with our own definition. The explicitness of override makes this clear to the reader and helps prevent mistakes.

Notice the formatting of the multiline parameter list for Snake and Moose—this is the recommended standard when you have too many parameters to fit on one line, for both constructors and functions.

Constraining Visibility

Kotlin provides access modifiers similar to those available in other languages like C++ or Java. These allow component creators to decide what is available to the client programmer. Kotlin’s access modifiers include the publicprivateprotected, and internal keywords. protectedis explained later in the book while internal is beyond the scope of this book.

An access modifier like public or private appears before the definition for a class, function or property. Each access modifier only controls the access for that particular definition.

public definition is available to everyone, in particular to the client programmer who uses that component. Thus, any changes to a publicdefinition will impact client code.

If you don’t provide a modifier, your definition is automatically public. For clarity in certain cases, programmers still sometimes redundantly specify public.

If you define a class, top-level function, or property as private, it is available only within that file:

// Summary2/Boxes.kt
package summary2

private var count = 0                   // [1]

private class Box(val dimension: Int) { // [2]
  fun volume() =
    dimension * dimension * dimension
  override fun toString() =
    "Box volume: ${volume()}"
}

private fun countBox(box: Box) {        // [3]
  println("$box")
  count++
}

fun countBoxes() {
  countBox(Box(4))
  countBox(Box(5))
}

fun main() {
  countBoxes()
  println("$count boxes")
}
/* Output:
Box volume: 64
Box volume: 125
2 boxes
*/

You can access private properties ([1]), classes ([2]), and functions ([3]) only from other functions and classes in the Boxes.kt file. Kotlin prevents you from accessing private top-level elements from another file.

Class members can be private:

// Summary2/JetPack.kt
package summary2

class JetPack(
  private var fuel: Double     // [1]
) {
  private var warning = false
  private fun burn() =         // [2]
    if (fuel - 1 <= 0) {
      fuel = 0.0
      warning = true
    } else
      fuel -= 1
  public fun fly() = burn()    // [3]
  fun check() =                // [4]
    if (warning)               // [5]
      "Warning"
    else
      "OK"
}

fun main() {
  val jetPack = JetPack(3.0)
  while (jetPack.check() != "Warning") {
    println(jetPack.check())
    jetPack.fly()
  }
  println(jetPack.check())
}
/* Output:
OK
OK
OK
Warning
*/
  • [1] fuel and warning are both private properties and can’t be used by non-members of JetPack.

  • [2] burn() is private, and thus only accessible inside JetPack.

  • [3] fly() and check() are public and can be used everywhere.

  • [4] No access modifier means public visibility.

  • [5] Only members of the same class can access private members.

Because a private definition is not available to everyone, you can generally change it without concern for the client programmer. As a library designer, you’ll typically keep everything as private as possible, and expose only functions and classes you want client programmers to use. To limit the size and complexity of example listings in this book, we only use private in special cases.

Any function you’re certain is only a helper function can be made private, to ensure you don’t accidentally use it elsewhere and thus prohibit yourself from changing or removing the function.

Exceptions

Consider toDouble(), which converts a String to a Double. What happens if you call it for a String that doesn’t translate into a Double?

// Summary2/ToDoubleException.kt

fun main() {
  // val i = "$1.9".toDouble()
}

Uncommenting the line in main() 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 current path of execution stops, and the exception object ejects from the current context. When an exception isn’t caught, the program aborts and displays a stack trace containing detailed information.

To avoid displaying exceptions by commenting and uncommenting code,atomictest.capture() stores the exception and compares it to what we expect:

// Summary2/AtomicTestCapture.kt
import atomictest.*

fun main() {
  capture {
    "$1.9".toDouble()
  } eq "NumberFormatException: " +
    """For input string: "$1.9""""
}

capture() is 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 your function can’t successfully produce the expected result is to return null. Later in Nullable Types we discuss how null affects the type of the resulting expression.

To throw an exception, use the throw keyword followed by the exception you want to throw, along with any arguments it might need. quadraticZeroes() in the following example solves the quadratic equationthat defines a parabola:

ax2 + bx + c = 0

The solution is the quadratic formula:

The Quadratic Formula

The example finds the zeroes of the parabola, where the lines cross the x-axis. We throw exceptions for two limitations:

  1. a cannot be zero.

  2. For zeroes to exist, b2 - 4ac cannot be negative.

If zeroes exist, there are two of them, so we create the Roots class to hold the return values:

// Summary2/Quadratic.kt
package summary2
import kotlin.math.sqrt
import atomictest.*

class Roots(
  val root1: Double,
  val root2: Double
)

fun quadraticZeroes(
  a: Double,
  b: Double,
  c: Double
): Roots {
  if (a == 0.0)
    throw IllegalArgumentException(
      "a is zero")
  val underRadical = b * b - 4 * a * c
  if (underRadical < 0)
    throw IllegalArgumentException(
      "Negative underRadical: $underRadical")
  val squareRoot = sqrt(underRadical)
  val root1 = (-b - squareRoot) / 2 * a
  val root2 = (-b + squareRoot) / 2 * a
  return Roots(root1, root2)
}

fun main() {
  capture {
    quadraticZeroes(0.0, 4.0, 5.0)
  } eq "IllegalArgumentException: " +
    "a is zero"
  capture {
    quadraticZeroes(3.0, 4.0, 5.0)
  } eq "IllegalArgumentException: " +
    "Negative underRadical: -44.0"
  val roots = quadraticZeroes(3.0, 8.0, 5.0)
  roots.root1 eq -15.0
  roots.root2 eq -9.0
}

Here we use the standard exception class IllegalArgumentException. Later you’ll learn to define your own exception types and to make them specific to your circumstances. Your goal is to generate the most useful messages possible, to simplify the support of your application in the future.

Lists

Lists are Kotlin’s basic sequential container type, and are part of the standard Kotlin package so they’re available without any imports. You create a read-only list using listOf() and a mutable list using mutableListOf():

// Summary2/ReadonlyVsMutableList.kt
import atomictest.eq

fun main() {
  val ints = listOf(5, 13, 9)
  // ints.add(11) // 'add()' not available
  for (i in ints) {
    if (i > 10) {
      println(i)
    }
  }
  val chars = mutableListOf('a', 'b', 'c')
  chars.add('d') // 'add()' available
  chars += 'e'
  println(chars)
}
/* Output:
13
[a, b, c, d, e]
*/

A basic List is read-only, and does not include modification functions. Thus, the modification function add() doesn’t work with ints.

for loops work well with Lists: for(i in ints) means i gets each value in ints.

chars is created as a MutableList; it can be modified using functions likeadd() or remove(). Alternatively, you can use += and -= to add or remove elements.

A read-only List is not the same as an immutable List, which can’t be modified at all. Here, we assign first, a mutable List, to second, a read-only List reference. The read-only characteristic of second doesn’t prevent the List from changing via first:

// Summary2/MultipleListReferences.kt
import atomictest.eq

fun main() {
  val first = mutableListOf(1)
  val second: List<Int> = first
  second eq listOf(1)
  first += 2
  // second sees the change:
  second eq listOf(1, 2)
}

first and second refer to the same object in memory. We mutate the List via the first reference, and then observe this change in the secondreference.

Here’s a List of Strings created by breaking up a triple-quoted paragraph. This shows the power of some of the standard library functions. Notice how those functions can be chained:

// Summary2/ListOfStrings.kt

fun main() {
  val jabber = """
    Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe:
    All mimsy were the borogoves,
      And the mome raths outgrabe.
  """.trim().split(Regex("\\W+"))
  println(jabber.take(5))
  println(jabber.slice(6..12))
  println(jabber.slice(6..18 step 2))
  println(jabber.sorted().takeLast(5))
  println(
    jabber.sorted().distinct().takeLast(5))
}
/* Output:
[Twas, brillig, and, the, slithy]
[Did, gyre, and, gimble, in, the, wabe]
[Did, and, in, wabe, mimsy, the, And]
[the, the, toves, wabe, were]
[slithy, the, toves, wabe, were]
*/

trim() produces a new String with the leading and trailing whitespace characters (including newlines) removed. split() divides the Stringaccording to its argument. In this case we use a Regex object, which creates a regular expression—a pattern that matches the parts to split. \W is a special pattern that means “not a word character,” and + means “one or more of the preceeding.” Thus split() will break at one or more non-word characters, and so divides the block of text into its component words.

In a String literal, \ precedes a special character and produces, for example, a newline character (\n), or a tab (\t). To produce an actual \ in the resulting String you need two backslashes: "\\". Thus all regular expressions require an extra \ to insert a backslash, unless you use a triple-quoted String"""\W+""".

take(n) produces a new List containing the first n elements. slice()produces a new List containing the elements selected by its Rangeargument, and this Range can include a step.

Note the name sorted() instead of sort(). When you call sorted() itproduces a sorted List, leaving the original List alone. sort() only works with a MutableList, and that list is sorted in place—the original List is modified.

As the name implies, takeLast(n) produces a new List of the last nelements. You can see from the output that “the” is duplicated. This is eliminated by adding the distinct() function to the call chain.

Parameterized Types

Type parameters allow us to describe compound types, most commonly containers. In particular, type parameters specify what a container holds. Here, we tell Kotlin that numbers contain a List of Int, while stringscontain a List of String:

// Summary2/ExplicitTyping.kt
package summary2
import atomictest.eq

fun main() {
  val numbers: List<Int> = listOf(1, 2, 3)
  val strings: List<String> =
    listOf("one", "two", "three")
  numbers eq "[1, 2, 3]"
  strings eq "[one, two, three]"
  toCharList("seven") eq "[s, e, v, e, n]"
}

fun toCharList(s: String): List<Char> =
  s.toList()

For both the numbers and strings definitions, we add colons and the type declarations List<Int> and List<String>. The angle brackets denote a type parameter, allowing us to say, “the container holds ‘parameter’ objects.” You typically pronounce List<Int> as “List of Int.”

A return value can also have a type parameter, as seen in toCharList(). You can’t just say it returns a List—Kotlin complains, so you must give the type parameter as well.

Variable Argument Lists

The vararg keyword is short for variable argument list, and allows a function to accept any number of arguments (including zero) of the specified type. The vararg becomes an Array, which is similar to a List:

// Summary2/VarArgs.kt
package summary2

fun varargs(s: String, vararg ints: Int) {
  for (i in ints) {
    print("$i ")
  }
  println(s)
}

fun main() {
  varargs("primes", 5, 7, 11, 13, 17, 19, 23)
}
/* Output:
5 7 11 13 17 19 23 primes
*/

A function definition may specify only one parameter as vararg. Any parameter in the list can be the vararg, but the final one is generally simplest.

You can pass an Array of elements wherever a vararg 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 *:

// Summary2/ArraySpread.kt
import summary2.varargs

fun main() {
  val array = intArrayOf(4, 5)      // [1]
  varargs("x", 1, 2, 3, *array, 6)  // [2]
  val list = listOf(9, 10, 11)
  varargs(
    "y", 7, 8, *list.toIntArray())  // [3]
}
/* Output:
1 2 3 4 5 6 x
7 8 9 10 11 y
*/

If you pass an Array of primitive types as in the example above, the Arraycreation function must be specifically typed. If [1] uses arrayOf(4, 5)instead of intArrayOf(4, 5)[2] produces an error: inferred type is Array<Int> but IntArray was expected.

The spread operator only works with arrays. If you have a List to pass as a sequence of arguments, first convert it to an Array and then apply the spread operator, as in [3]. Because the result is an Array of a primitive type, we must use the specific conversion function toIntArray().

Sets

Sets are collections that allow only one element of each value. A Setautomatically prevents duplicates.

// Summary2/ColorSet.kt
package summary2
import atomictest.eq

val colors =
  "Yellow Green Green Blue"
    .split(Regex("""\W+""")).sorted()  // [1]

fun main() {
  colors eq
    listOf("Blue", "Green", "Green", "Yellow")
  val colorSet = colors.toSet()        // [2]
  colorSet eq
    setOf("Yellow", "Green", "Blue")
  (colorSet + colorSet) eq colorSet    // [3]
  val mSet = colorSet.toMutableSet()   // [4]
  mSet -= "Blue"
  mSet += "Red"                        // [5]
  mSet eq
    setOf("Yellow", "Green", "Red")
  // Set membership:
  ("Green" in colorSet) eq true        // [6]
  colorSet.contains("Red") eq false
}
  • [1] The String is split() using a regular expression as described earlier for ListOfStrings.kt.

  • [2] When colors is copied into the read-only Set colorSet, one of the two "Green" Strings is removed, because it is a duplicate.

  • [3] Here we create and display a new Set using the + operator. Placing duplicate items into a Set automatically removes those duplicates, so the resulting set is the same.

  • [4] toMutableSet() produces a MutableSet from a read-only Set.

  • [5] For a MutableSet, the operators += and -= add and remove elements, as they do with MutableLists.

  • [6] Test for Set membership using in or contains()

The normal mathematical set operations such as union, interp, difference, etc., are all available.

Maps

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(). Each key is separated from its associated value by to:

// Summary2/ASCIIMap.kt
import atomictest.eq

fun main() {
  val ascii = mapOf(
    "A" to 65,
    "B" to 66,
    "C" to 67,
    "I" to 73,
    "J" to 74,
    "K" to 75
  )
  ascii eq
    "{A=65, B=66, C=67, I=73, J=74, K=75}"
  ascii["B"] eq 66                   // [1]
  ascii.keys eq "[A, B, C, I, J, K]"
  ascii.values eq
    "[65, 66, 67, 73, 74, 75]"
  for (entry in ascii) {             // [2]
    print("${entry.key}:${entry.value},")
  }
  println()
  for ((key, value) in ascii)        // [3]
    print("$key:$value,")
  println()
  val mutable = ascii.toMutableMap() // [4]
  mutable.remove("I")
  mutable eq
    "{A=65, B=66, C=67, J=74, K=75}"
  mutable.put("Z", 90)
  mutable eq
    "{A=65, B=66, C=67, J=74, K=75, Z=90}"
  mutable.clear()
  mutable["A"] = 100
  mutable eq "{A=100}"
}
/* Output:
{A=65, B=66, C=67, I=73, J=74, K=75}
66
[A, B, C, I, J, K]
[65, 66, 67, 73, 74, 75]
A:65,B:66,C:67,I:73,J:74,K:75,
A:65,B:66,C:67,I:73,J:74,K:75,
{A=65, B=66, C=67, J=74, K=75}
{A=65, B=66, C=67, J=74, K=75, Z=90}
{A=100}
*/
  • [1] With a Map, the [] operator is used for lookup using a key. You can produce all the keys using keys and all the values using values. Accessing keys produces a Set because all keys in a Map must already 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 key-value pairs as you iterate.

  • [4] You can create a MutableMap using mutableMapOf(). Here, we create a MutableMap from a read-only Map using toMutableMap(). Now we can perform operations that modify mutable, such as remove()put(), and clear(). Square brackets can assign a new key-value pair into mutable. You can also add a pair by saying map += key to value.

Property Accessors

This appears to be straightforward access to the property i:

// Summary2/PropertyReadWrite.kt
package summary2
import atomictest.eq

class Holder(var i: Int)

fun main() {
  val holder = Holder(10)
  holder.i eq 10 // Read the 'i' property
  holder.i = 20  // Write to the 'i' property
}

However, Kotlin calls functions to perform the read and write operations. The default behavior of those functions is to read and write the data stored in i. By creating property accessors, you change the actions that occur during reading and writing.

The accessor used to fetch the value of a property is called a getter. To create your own getter, define get() immediately after the property declaration. The accessor used to modify a mutable property is called asetter. To create your own setter, define set() immediately after the property declaration. The order of definition of getters and setters is unimportant, and you can define one without the other.

The property accessors in the following example imitate the default implementations. They display additional information so you can see that the property accessors are indeed called during reads and writes. We indent the get() and set() functions to visually associate them with the property, but the actual association happens because they are defined immediately after that property:

// Summary2/GetterAndSetter.kt
package summary2

class GetterAndSetter {
  var i: Int = 0
    get() {
      println("get()")
      return field
    }
    set(value) {
      println("set($value)")
      field = value
    }
}

fun main() {
  val gs = GetterAndSetter()
  gs.i = 2
  println(gs.i)
}
/* Output:
set(2)
get()
2
*/

Inside the getter and setter, the stored value is manipulated indirectly using the field keyword, which is only accessible within these two functions. You can also create a property that doesn’t have a field, but simply calls the getter to produce a result.

If you declare a private property, both accessors become private. You can make the setter private and the getter public. This means you can read the property outside the class, but only change its value inside the class.

Exercises and solutions for this atom can be found at AtomicKotlin.com.

參考資料:

https://www.atomickotlin.com/atomickotlin/