文章目录
- 1 可空性
- 1.1 可空类型
- 1.2 安全调用运算符 "?."
- 1.3 Elvis运算符 "?:"
- 1.4 安全转换 "as?"
- 1.5 非空断言 "!!"
- 1.6 let函数
- 1.7 延迟初始化的属性
- 1.8 可空类型的扩展
- 1.9 类型参数的可空性
- 1.10 可空性和java
- 1.10.1 平台类型
- 1.10.2 继承
- 2 基本数据类型和其他类型
- 2.1 基本数据类型:Int、Boolean及其他
- 2.2 可空的基本数据类型:Int?、Boolean?及其他
- 2.3 数字转换
- 2.4 "Any"和"Any?":根类型
- 2.5 Unit类型:Kotlin的"void"
- 2.6 Nothing类型:“这个函数永不返回”
- 3 集合与数组
- 3.1 可空性和集合
- 3.2 只读集合与可变集合
- 3.3 Kotlin集合和java
- 3.4 对象和基本数据类型的数组
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())
}
}
lateinit
和 by lazy
虽然都是延迟初始化,但不同的地方在于:
lateinit
是变量声明为var
时才使用,by lazy
是变量声明为val
初始化后不可变时使用lateinit
不能用于基本数据类型,如Int
、Long
等,需要使用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
的表达式之后的语句都是无法执行的,就好像 return
和 break
的作用)
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中的 ArayList
和 HashSet
都继承了Kotlin的可变接口。
同样的,Kotlin中的Map也分为可读和可变(它并没有继承Collection或是Iterable):Map
和 MutableMap
。
集合类型 | 只读 | 可变 |
List | listOf | mutableListOf、arayListOf |
Set | setOf | mutableSetOf、hashSetOf、linkedSetOf、sortedSetOf |
Map | mapOf | mutableMapOf、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
装箱类型,而不是基本数据类型。
如果想要使用基本数据类型数组,可以使用 IntArray
、ByteArray
、CharArray
、BooleanArray
这些函数创建,分别对应java中的 int[]
、byte[]
、char[]
等。
>>val fiveZeros = IntArray(5) // 创建基本数据类型的数组,构造传递创建的数组大小
>>val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0) // 创建基本数据类型数组的另外一种方式
由于kotlin对原始类型有特殊的优化(主要体现在避免了自动装箱带来的开销),所以建议优先使用原始类型数组。