Kotlin入门系列:第五章 Kotlin的类型系统

1 可空性

现代编程语言包括Kotlin解决 NullPointerException 问题的方法是把运行时的错误转变为编译期的错误。通过支持作为类型系统的一部分的可空性,编译器就能在编译期发现很多潜在的错误,从而减少运行时抛出异常的可能性。

1.1 可空类型

假设在java中有这样的一个方法:

int strLen(String s) {
	return s.length();
}

该方法存在的问题是,在调用时是有可能这样调用:

strLen(null);

如果没有对参数 String s 进行非空判断,就会出现 NullPointerException

而在Kotlin中,对于传递的参数默认是非空的(即 not-null),如果在Kotlin中写同样的方法,在编译期间该方法将会被标记为错误:

fun strLen(s: String) = s.length()

>> strLen(null)
ERROR:Null can not be a value of a non-null type String

如果你允许调用的方法传递给它的参数可以为null,需要显式地在类型名称后面加上问号来标记:

fun strLen(s: String?) = s.length()

// 其他类型也同理
Int?、String?、Type?

一旦你有一个可空类型的值,能对它进行的操作也会受到限制:

// 不能再调用它的方法
>>fun strLenSafe(s: String?) = s.length()
ERROR:Only safe (?.) or non-null asserted(!!.) calls are allowed on a nullable receiver of type kotlin.String?

// 不能把它赋值给非空类型的变量
>>val x: String? = null
>>var y: String = x
ERROR:Type mismatch: inferred type is String? but String was excepted

// 不能把可空类型的值传递给拥有非空类型参数的函数
>>strLen(x)
ERROR:Type mismatch: inferred type is String? but String was excepted

如果希望可空类型的值可以通过编译,可以添加判断:

fun strLenSafe(s: String) : Int = if (s != null) s.length() else 0

>>val x: String? = null
>>println(strLenSafe(x))
>>println(strLenSafe("abc"))

输出:
0
3

如果确认可空类型的值不会为空,也可以使用断言的方式:

var name: String = "Sam"
var name2: String? = null

name = name2!!

1.2 安全调用运算符 “?.”

安全调用运算符 ?. 允许你把一次null检查和一次方法调用合并成一个操作。使用安全调用运算符时试图调用一个非空值的方法,这次方法调用会被正常执行。但如果值是null,这次调用不会发生,而整个表达式的值为null。

s?.toUpperCase()
等价于
if (s != null) s.toUpperCase() else null

fun printAllCaps(s: String?) {
	val allCaps: String? = s?.toUpperCase()
	println(allCaps)
}
>>printlnAllCaps("abc")
>>printlnAllCaps(null)

输出:
ABC
null

安全调用不光可以调用方法,也能用来访问属性:

class Employee(val name: String, val manager: Employee?)

fun managerName(employee: Employee) : String? = employee.manager?.name

>>val ceo = Employee("Da Boss", null)
>>val developer = Employee("Bob Smitch", ceo)
>>println(managerName(developer))
>>println(managerName(ceo))

输出:
Da Boss
null

如果你的对象图中有多个可空类型的属性,通常可以在同一个表达式中方便地使用多个安全调用:

class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
	val country = this.company?.address?.country // 多个安全调用链接在一起
	return if (country != null) country else "Unknown" // 还是要进行判断值是否为空提供默认值,下面讲解Elvis运算符解决该问题
}
>>val person = Person("Dmitry", null)
>>println(person.countryName())

输出:
Unknown

总结:

安全调用运算符?.定义:
foo?.bar()
等价于
foo != null ? foo.bar() : null

实际上,如果将kotlin反编译为java代码,可以发现运算符 ?. 其实是语法糖,最终的还是使用的 if (xxx != null) 兼容java处理。

1.3 Elvis运算符 “?:”

Evlis运算符(或者null合并运算符)可以提供代替null的默认值。

fun foo(s: String?) {
	val t: String = s ?: "" // 如果s == null默认值为""等价于if (s == null) "" else s
}

Elvis运算符通常和安全调用运算符一起使用。

fun strLenSafe(s: String?): Int = s?.length ?: 0

>>println(strLenSafe("abc"))
>>println(strLenSafe(null))

输出:
3
0

fun Person.countryName() = company?.adress?.country ?: "Unknown"

Elvis运算符也可以配合异常使用。

class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun printShippingLabel(person: Person) {
	// if (person.company != null) company.address : throw Exception
	val address = person.company?.address ?: throw IllegalArgumentException("No address")
	with(address) {
		println(streenAddress)
		println("$zipCode $city, $country")
	}
}

>>val address = Address("Elsestr. 47", 80687, "Munich", "Gemany")
>>val jetbrains = Company("JetBrains", address)
>>val person = Person("Dmitry", jetbrains)
>>printShippingLabel(person)
>>printShiipingLabel(Person("Alexey", null))

输出:
Elsestr. 47
80687 Munich, Germany

java.lang.IllegalArgumentException: No address

如果我们遇到不想抛出异常呢?

我们可以使用 Either 的子类型,在kotlin并没有Either类,但是我们可以自己定义一个:

data class Glasses(val degreeOfMyopia: Double)
data class Student(val glasses: Glasses?)
data class Seat(val student: Student)

// Either只有两个子类型:Left,Right
// Either[A, B]对象包含的是A实例,那么它就是Left实例,否则就是Right实例
sealed calss Either<A, B> {
	class Left<A, B>(val value: A): Either<A, B>()
	class Right<A, B>(val value: B): Either<A, B>()
}

fun getDegree(seat: Seat?): Either<Error, Double> {
    return seat?.student?.glasses?.let {
        return Either.Right(it.degreeOfMyopia)
    } ?: Either.Left(java.lang.Error("non glasses"))
}

总结:

Elvis运算符?:定义:
一般配合安全调用运算符?.使用,Elvis运算符提供返回null时的默认值
foo != null ? foo.bar : default
等价于
foo?.bar ?: default

1.4 安全转换 “as?”

在java中进行类型转换会使用 instanceof 判断是否为对应类型,然后才进行类型转换,但如果疏忽没有加上类型判断将会抛出 ClassCastException

在Kotlin中,类型判断使用 is 运算符,类型转换使用 as 运算符,Kotlin也提供了更好的安全转换 as?,尝试把值转换成指定类型,如果不合适就返回null。

一种常见的模式是安全转换和Elvis运算符配合使用。

class Person(val firstName: String, val lastName: String) {
	override fun equals(o: Any?): Boolean {
		val otherPerson = o as? Person ?: return false

		// 在智能转换后,变量otherPerson自动转换为Person类型
		return otherPerson.firstName == firstName && otherPerson.lastName == lastName
	}

	override fun hashCoe(): Int = firstName.hashCode() * 37 + lastName.hashCode()
}

>>val p1 = Person("Dmitry", "Jemerov")
>>val p2 = Person("Dmitry", "Jemerov")
>>println(p1 == p2)
>>println(p1.equals(42))

输出:
true
false

总结:

安全转换运算符as?定义:
foo as Type ? (Type)foo : null
等价于
foo as? Type

1.5 非空断言 “!!”

fun ignoreNulls(s: String?) {
	val sNotNull: String = s!! // s != null ? s : throw NullPointerException
	println(sNotNull.length)
}

>>ignoreNulls(null)
Exception in threa "main" kotlin.KotlinNullPointerException at <...>.igoreNulls(07_NotnullAssertions.kt.2)

如果函数中的参数s为null,Kotlin会在运行时抛出一个异常,但是抛出异常的位置不是在调用位置 sNotNull.length,而是在非空断言那一行。本质上,非空断言就如在告诉编译器:“我知道这个值不为null,如果我错了我准备好了接收这个异常。“

某些问题适合用非空断言来解决。当你在一个函数中检查一个值是否为null,而在另一个函数中使用这个值时,这种情况下编译器无法识别这种用法是否安全。如果你确信这样的检查一定在其他某个函数中存在,你可能不想在使用这个值之前重复检查,这时你就可以使用非空断言。

class CopyRowAction(val list: JList<String>): AbstractAction() {
	override fun isEnabled(): Boolean = list.selectedValue != null

	override fun actionPerformed(e: ActionEvent) { // 只会在isEnabled返回true时被调用
		val value = list.selectedValue!! // 如果不想使用非空断言,可以使用Elvis运算符提前返回 list.selectedValue ?: return
	}
}

需要牢记注意事项:当你使用 !! 并且它的结果时异常时,异常调用栈的跟踪信息只表明异常发生在哪一行代码,而不会表明异常发生在哪一个表达式。要避免在同一行使用多个非空断言。

person.company!!.address!!.country // 避免在同一行多次调用非空断言!

1.6 let函数

let 函数允许你对表达式求值,检查求值结果是否为null,并把结果保存为一个变量。

fun sendEmailTo(email: String) { ... }

>>val email: String? = ...
>>sendEmailTo(email)  // Kotlin在非空函数中传递可空的实参会在编译器提示错误,可以使用let函数处理

let 函数做的所有事情就是把一个调用它的对象变成lambda表达式的参数。如果结合安全调用语法,它能有效地把调用let函数的可空对象,转变成非空类型。

email?.let { email->sendEmailTo(email) } // let函数只在email非空时才被调用

fun sendEmailTo(email: String) {
	println("Sending email to $email")
}

>>var email: String? = "yole@example.com"
>>email?.let { sendEmailTo(it) }
>>email = null
>>email?.let { sendEmailTo(it) }

输出:
Sending email to yole@example.com // 只打印出了第一个,第二个eamil==null没有打印

val person: Person? = getTheBestPersonInTheWorld()
if (person != null) sendEmailTo(person.eamil)
等价于
getTheBestPersonInTheWorld()?.let { sendEmailTo(it) } // 如果getTheBestPersonInTheWorld() == null则不会执行

1.7 延迟初始化的属性

Kotlin通常要求你在构造方法中初始化所有属性,如果某个属性是非空类型,你就必须提供非空的初始化值。否则,你就必须使用可空类型。如果你这样做,该属性的每一次访问都需要null检查或者!!运算符。

class MyService {
	fun performAction() : String = "foo"
}

class MyTest {
	private var myService: MyService? = null

	@Before fun setUp() {
		myService = MyService()
	}

	@Test fun testAction() {
		Assert.assertEquals("foo", myService!!.performAction())
	}
}

因为 myService 是可空的,所以在每次使用时都必须检查非空。为了解决这个问题,可以把 myService 属性声明成可以延迟初始化的,使用 lateinit 修饰符来完成。

class MyService {
	fun performAction(): String = "foo"
}

class MyTest {
	private lateinit var myService: MyService // 声明一个不需要初始化器的非空类型的属性

	@Before fun setUp() {
		myService = MyService()
	}

	@Test fun testAction() {
		Assert.assertEquals("foo", myService.performAction())
	}
}

注意,延迟初始化的属性都是var,因为需要在构造方法外修改它的值,而val属性会被编译成必须在构造方法中初始化的final字段。尽管这个属性是非空类型,但是你不需要在构造方法中初始化它。如果在属性被初始化之前就访问了它,会得到异常 lateinit property myService has not been initialized

延迟初始化除了使用 lateinit 之外,还有一种 by lazy

class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private val myService: MyService by lazy {
        MyService() // 在lazy中设置初始化MyService对象的代码
    }

    fun testAction() {
        println("foo" == myService.performAction())
    }
}

lateinitby lazy 虽然都是延迟初始化,但不同的地方在于:

  • lateinit 是变量声明为 var 时才使用,by lazy 是变量声明为 val 初始化后不可变时使用

  • lateinit 不能用于基本数据类型,如 IntLong 等,需要使用 Integer 包装类替代

lazy 属性可以使用三种模式:

  • LazyThreadSafetyMode.SYNCHRONIZED:lazy开启同步锁,同一时刻只允许一个线程对lazy属性进行初始化,默认使用该模式,线程安全的

  • LazyThreadSafetyMode.PUBLICATION:确认该属性可以并行执行,没有线程安全问题,可以使用该模式

  • LazyThreadSatetyMode.NONE:不会有线程方面的开销,但也不会有任何线程安全的保证

val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) {
	// 并行模式
	if (color == "yellow") "male" else "female"
}

val sex: String by lazy(LazyThreadSafetyMode.NONE) {
	// 不做任何线程处理
	if (color == "yellow") "male" else "female"
}

1.8 可空类型的扩展

为可空类型定义扩展函数是一种更强大的null值处理方式。可以允许接收者为null的(扩展函数)调用,并在该函数中处理null,而不是在确保变量不为null之后再调用它的方法。只有扩展函数才能做到这一点,普通成员方法的调用是通过对象实例来分发的,因此实例为null时(成员方法)永远不能被执行。

fun verifyUserInput(input: String) {
	if (input.isNullOrBlank()) {  // input为可空类型的值,不需要进行检查,会在isNullOrBlank()方法中检查
		println("please fill in the required fields")
	}
}

// isNullOrBlank()为一个扩展函数,可以对可空的值调用进行null检查,第二个this使用了智能转换
fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()

>>verifyUserInput(" ")
>>verifyUserINput(null)

输出:
please fill in the required fields
please fill in the required fields

let 函数也能被可空的接收者调用,但它并不检查值是否为null。如果你在一个可空类型值上调用 let 函数,而没有使用安全调用运算符,lambda的实参将会是可空的。

>>val person: Person? = ....
>>person.let { sendEmailTo(it) } // person是可空的,应该用安全调用运算符处理 person?.let { sendEmailTo(it) }
ERROR: Type mismatch: inferred type is Person? but Person was excepted

1.9 类型参数的可空性

Kotlin中所有泛型类和泛型函数的类型参数默认都是可空的。任何类型,包括可空类型在内,都可以替换类型参数。这种情况下,使用类型参数作为类型的声明都允许为null,尽管类型参数T并没有用问号结尾。

fun <T> printHashCode(t: T) { // T被推导成Any?
	println(t?.hashCode())
}

>>printHashCode(null)

输出:
null

 fun <T: Any> printHashCode(t: T) {
	println(t.hashCode())
}

>>printHashCode(null)
Error:Type parameter bound for 'T' is not satisfied

>>printHashCode(42)
42

1.10 可空性和java

在Kotlin做的可空性处理后,转换为java后会进行一些转换处理。

在java中的一些注解,比如 @Nullable在Kotlin中转换为 Type? 比如 String?,而 @NotNull 在Kotlin中转换为 Type 比如 String

Kotlin可以识别多种不同风格的可空性注解,包括 javax.annotation 包的注解、android.support.annotation Android的注解和 org.jetbrains.annotations JetBrains工具支持的注解。如果在java中设置的注解在Kotlin中不存在,java类型将会被转换为Kotlin的平台类型。

1.10.1 平台类型

平台类型 其实就是Kotlin不知道可空性信息的类型,既可以把它当作可空类型处理,也可以当作非空类型处理,这意味着要像在java中对这个数据做null判断处理,否则数据为null将会抛出空指针异常。

public class Person {
	private final String name;

	public Person(String name) {
		this.name = name;
	}

	// 当转换为Kotlin时,将会是平台类型,需要在Kotlin中做空判断
	public String getName() {
		return name;
	}
}

fun yellAt(person: Person) {
	println(person.name.toUpperCase())
	// println((person.name ?: "Anyone").toUpperCase) // 进行安全处理
}

>>yellAt(Person(null))
java.lang.IllegalArgumentException:Parameter specified as non-null
is null:method toUpperCase, parameter $receiver

在Kotlin中不能声明一个平台类型的变量,这些类型只能来自java代码,但你可能会在IDE的错误消息中见到它们:

>>val i: Int = person.name
ERROR:Type mismatch:inferred type is String! but Int was expected

String! 表示法被Kotlin编译器用来表示来自java代码的平台类型。你不能在自己的代码中使用这种语法。而感叹号通常与问题的来源无关,所以通常可以忽略它。它只是强调类型的可空性是未知的。

因为Kotlin平台类型可以是可空也可以是非空,所以下面两种声明都是有效的:

// 如果你试图用来自java的null值给一个非空的Kotlin变量赋值,在赋值的瞬间你就会得到异常
// 因为Kotlin编译器会为每一个非空的参数生成一个非空断言,即使没有访问过该参数也是会给你一个异常
>>val s: String? = person.name
>>val s: String = person.name

1.10.2 继承

当在Kotlin中重写java方法时,可以选择把参数和返回类型定义成可空的,也可以选择把它们定义成非空的。

interface StringProcessor {
	void process(String value);
}

class StringPrinter: StringProcessor {
	override fun process(value: String) {
		println(value)
	}
}

class NullableStringPrinter: StringProcessor {
	override fun process(value: String?) {
		if (value != null) println(value)
	}
}

在实现java类或者接口的方法时一定要搞清楚它的可空性。因为方法的实现可以在非Kotlin的代码中被调用,Kotlin编译器会为你声明的每一个非空的参数生成非空断言。如果java代码传给这个方法一个null值,断言将会出发,你会得到一个异常,即使你从没有在你的实现中访问过这个参数值。

2 基本数据类型和其他类型

在这里插入图片描述

2.1 基本数据类型:Int、Boolean及其他

java把基本数据类型和引用类型做了区分:一个基本数据类型(比如 int)的变量直接存储了它的值,而一个引用类型(比如 String)的变量存储的是指向包含该对象的内存地址的引用。在集合上,java也提供了包装类型(比如 java.lang.Integer)对基本数据类型进行封装。

Kotlin并不区分基本数据类型和包装类型,使用的永远是同一个类型:

val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)

此外,你还能对一个数字类型的值调用方法:

fun showProgress(progress: Int) {
	val percent = progress.coerceIn(0, 100) // 限制progress范围在[0, 100]
	println("We're ${percent}% done!")
}

>>showProgress(146)

输出:
We're 100% done!

在Kotlin中,虽然使用的只有同一个类型(比如Int),但Kotlin在运行时,在大多数情况下——对于变量、属性、参数和返回类型——Kotlin的 Int 类型会被编译成java基本数据类型 int,唯一不可行的例外是泛型类,比如集合,泛型类型参数的基本类型会被编译成对应java的包装类型。

Int 这样的Kotlin类型在底层可以轻易地编译成对应的java基本数据类型,因为两种类型都不能存储null引用。反过来也差不多:当你在Kotlin中使用java声明时,java基本据类型会变成非空类型(而不是平台类型),因为它们不能持有null值。

如果你有需求,就是要在Kotlin调用Java封装类型的方法,可以尝试使用反射的方式实现。

2.2 可空的基本数据类型:Int?、Boolean?及其他

// Person中的age是Int?可空类型,将会被当成是Integer来表示存储,而不是int基本数据类型
data class Person(val name: String, val age: Int? = null) {
	fun isOlderThan(other: Person): Boolean? {
		// age参数是可空的,所以不能age直接和other.age进行比较,因为它们任何一个都可能为null
		// 所以必须检查两个值不为null,编译器才允许你正常地比较它们
		if (age == null || other.agr == null) return null
		return age > other.age
	}
}

>>println(Person("Sam", 35).isOlderThan(Person("Amy", 42))
>>println(Person("Sam", 35).isOlderThan(Person("Jane"))

输出:
false
null

如果你用基本数据类型作为泛型类的类型参数,那么Kotlin会使用该类型的包装形式:

// 这里存储的值将会是Integer包装类型
val listOfInts = listOf(1, 2, 3)

总结

val x1: Int = 18 // kotlin
val x2: Int? = 18 // kotlin
int x3 = 18; // java
Integer x4 = 18; // java

反编译代码:
BIPUSH 18
ISTORE 1

BIPUSH 18
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 1

bipush 18
istore_1

bipush 18
invokestatic #2 <java/lang/Integer.valueOf>
astore_1
  • Int 等同于java的基本数据类型 int

  • Int? 等同于java的装箱类型 Integer

2.3 数字转换

Kotlin在基本数据类型转换时,需要显式地进行数据类型转换,比如 Int 转为 Long 或者相反,否则Kotlin不会编译。

val i = 1
val l: Long = i // 不会编译

val l: Long = i.toLong() // 要进行转换

val x = 1
val list = listOf(1L, 2L, 3L)
x.toLong() in list // 需要进行数据类型转换

每一种基本数据类型(除了Boolean)都定义有转换函数:toByte()toShort()toChar()等,这些函数支持双向转换,即可以把小范围的类型扩展到大范围,比如 Int.toLong(),也可以把大范围的类型截取到小范围,比如 Long.toInt()

在比较装箱值时,equals() 不仅会检查它们存储的值,还会比较装箱类型。比如 new Integer(42).equals(new Long(42)) 将会是 false

当你书写数字字面值时,一般不需要使用转换函数。一种可能性是用这种特殊的语法来显式地标记常量的类型,比如42L或者42.0f。而且即使你没有用这种语法,当你使用数字字面量去初始化一个类型已知的变量时,又或者把字面量作为实参传给函数时,必要的转换会自动发生。

fun foo(l: Long) = println(l)

val b: Byte = 1 // 常量有正确的类型
val l = b + 1L // +可以进行字节类型和长整型参数的计算
foo(42) // 编译器认为42是一个长整型

42

字符串也提供了转换成基本数据类型的函数:

>>println("42".toInt())

42

2.4 “Any"和"Any?”:根类型

Kotlin中的 Any 类型相当于java中的 Object 类型,它是所有非空类型的超类,如果要表示为可空类型,就是 Any?。Kotlin类都包含下main三个方法:toString()equals()hashCode(),这些方法都继承自 Any,但 Any 并不能使用Object的 wait()notify() 这些方法,可以通过手动转换成 java.lang.Object 来调用这些方法。

val answer: Any = 42 // Any是类的类型,所以会自动装箱为Integer类型

2.5 Unit类型:Kotlin的"void"

在大多数情况下,可以将 Unit 类型认为是java的 void,底层也确实会将 Unit 类型转为 void,比如一个方法不返回任何类型:

fun f(): Unit {}

// 可以隐匿Unit,和上面的写法没什么区别
fun f() {} 

Unit 类型同时也和java的 void 类型不同:Unit 是一个完备的类型,可以作为类型参数,而 void 却不行。Unit 类型只存在一个值也叫 Unit,并且在函数中会被隐式地返回。当你在重写返回泛型参数的函数时这非常有用,只需要让方法返回 Unit 类型的值:

interface Processor<T> {
	fun process(): T
}

class NoResultProcessor: Processor<Unit> {
	override fun process() {
		// 不需要写return,因为编译器会自动的返回retur Unit
	}
}

2.6 Nothing类型:“这个函数永不返回”

Nothing 类型没有任何值,只有被当作函数返回值使用,或者被当作泛型函数返回值的类型参数使用才会有意义。(即任何返回值为 Nothing 的表达式之后的语句都是无法执行的,就好像 returnbreak 的作用)

fun fail(message: String): Nothing {
	throw IllegalStateException(message)
}

>>fail("Error occurred")

输出:
java.lang.IllegalStateException:Error occurred

返回Nothing的函数可以放在Elvis运算符的右边来做先决条件的检查:

val address = company.address ?: fail("No address")
println(address.city)

编译器知道这种Nothing返回类型的函数从不正常终止,然后在分析调用这个函数的代码时利用这个信息。上面的例子中,编译器会把address的类型判断成非空,因为它为null时的分支处理会始终抛出异常。

3 集合与数组

3.1 可空性和集合

List<Int?> 表示列表不为null,列表元素可以为null;List<Int>? 表示列表可以为null,列表元素不为null;List<Int?>? 表示列表和元素都可以为null。

// 返回的列表元素是可空的
fun readNumbers(reader: BufferReader): List<Int?> {
	val result = ArrayList<Int?>()
	for (line in reader.lineSequence()) {
		// 在Kotlin 1.1以上,可以使用String.toIntOrNull()简化下面的操作
		try {
			val number = line.toInt()
			result.add(number)
		} catch (e: NumberFormatException) {
			result.add(null)
		}
	}
}

fun addValidNumbers(numbers: List<Int?>) {
	// 以下遍历包含可空值的集合并过滤掉null的操作,可以使用Kotlin提供的函数filterNotNull()完成
	// val validNumbers = numbers.filterNotNull(),但是返回的就是List<Int>都是非空的集合
	var sumOfValidNumbers = 0
	var invalidNumbers = 0
	for (number in numbers) {
		if (number != null) {
			sumOfValidNumbers += number
		} else {
			invalidNumbers++
		}
	}
	println("Sum Of valid numbers:$sumOfValidNumbers")
	println("Invalid numbers:$invalidNumbers")
}

>>val reader = BufferReader(StringReader("1\nabc\n42"))
>>val numbers = readNumbers(reader)
>>addValidNumbers(numbers)

输出:
Sum of valid numbers:42
Invalid numbers:1

3.2 只读集合与可变集合

Kotlin的集合设计和java不同的另一项重要特质是,它把访问集合数据的接口和修改集合数据的接口分开了。

kotlin.collections.Collection 接口提供 size()iterator()contains()接口方法,并没有java中列表的对数据的操作,它是只读的集合;kotlin.collections.MutableCollection 接口继承自 kotlin.collections.Collection,提供了add()remove()clear()对集合元素修改的操作,这样就分离的集合元素的读和写操作。通过在方法中传递Collections 我们就能知道它是只读的集合,而如果传递的是 MutableCollection 就能知道集合是可读写的。

在这里插入图片描述

fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
	for (item in source) { // source是可读的
		target.add(item) // target是可写的
	}
}

>>val source: Collection<Int> = arrayListOf(3, 5, 7)
>>val target: MutableCollection<Int> = arrayListOf(1)
>>copyElements(source, target)
>>println(target)

输出:
[1, 3, 5, 7]

>>val targt: Collection<Int = arrayListOf(1) // 错误,target在copyElements()中应该是可写的集合参数
>>copyElements(source, target)

Kotlin也和java一样,只读集合并不总是线程安全的,需要注意 concurrentModificationException

3.3 Kotlin集合和java

Kotlin的集合有两种表示:一种时只读的,另一种是可变的。
在这里插入图片描述

从上图可以看出,java中的 ArayListHashSet 都继承了Kotlin的可变接口。

同样的,Kotlin中的Map也分为可读和可变(它并没有继承Collection或是Iterable):MapMutableMap

集合类型只读可变
ListlistOfmutableListOf、arayListOf
SetsetOfmutableSetOf、hashSetOf、linkedSetOf、sortedSetOf
MapmapOfmutableMapOf、hashMapOf、linkedMapOf、sortedMapOf

需要主要的是,java并不会区分只读集合与可变集合,即Kotlin中把集合声明成只读的Collection,java代码也能够修改这个集合。Kotlin编译器不能完全地分析java代码到底对集合做了什么,因此Kotlin无法拒绝向可以修改集合的java代码传递只读Collection。

Java CollectionUtils.java
public class CollectionUtils {
	public static List<String> uppercaseAll(List<String> items) {
		for (int i = 0; i < items.size(); i++) {
			items.add(i, items.get(i).toUpperCase());
		}
	}
}

Kotlin Collections.kt
fun printIntUppercase(list: List<String>) { // 声明只读的参数
	println(CollectionUtils.uppercaseAll(list)) // 调用可以修改集合的java函数
	println(list.first()) 
}

>>val list = listOf("a", "b", "c")
>>printIntUppercase(list)

输出:
[A, B, C]
A

3.4 对象和基本数据类型的数组

Kotlin中的一个数组是一个带有类型参数的类,其元素类型被指定为相应的类型参数。

在Kotlin中创建数组的方式:

  • arrayOf():它包含的元素是指定为该函数的实参

  • arrayOfNulls():创建一个给定大小的数组,包含的是null元素。当然,它只能用来创建包含元素类型可空的数组

  • Array():构造方法接收数组的大小和一个lambda表达式,调用lambda表达式来创建每一个数组元素。这就是使用非空元素类型来初始化数组,但不用显式地传递每个元素的方式

>>val letters = Array<String>(26) { i -> ('a' + i).toString() } // i是数组的下标
>>println(letters.joinToString(""))

输出:
abcdefghijklmnopqrstuvwxyz

Kotlin中创建数组最常见的情况之一是需要调用参数为数组的java方法时,或是调用带有 vararg 参数的Kotlin函数时。这种情况下,通常已经将数据存储在集合中,只需将其转换为数组即可,可以使用 toTypedArray 方法。

>>val strings = listOf("a", "b", "c")
>>println("%s/%s/%s".format(*strings.toTypedArray())) // 期望vararg参数时使用展开运算符(*)传递数组

输出:
a/b/c

数组类型的类型参数始终会变成对象类型。就是说 Array<Int> 创建的数组,里面存储的是java的 Integer 装箱类型,而不是基本数据类型。

如果想要使用基本数据类型数组,可以使用 IntArrayByteArrayCharArrayBooleanArray 这些函数创建,分别对应java中的 int[]byte[]char[] 等。

>>val fiveZeros = IntArray(5) // 创建基本数据类型的数组,构造传递创建的数组大小
>>val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0) // 创建基本数据类型数组的另外一种方式

由于kotlin对原始类型有特殊的优化(主要体现在避免了自动装箱带来的开销),所以建议优先使用原始类型数组。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值