Kotlin 基础
类
对比下Java的类Person与Kotlin的类Person区别:
// java
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
// 提供一个getter访问器
public String getName() {
return name;
}
}
// kotlin
// 在Kotlin中,public是默认的可见性,可以省略
class Person(val name: String)
属性
在Kotlin中,在类中声明一个属性和声明一个变量一样,使用 val(只读的) 和 var(可变的) 关键字。
如果属性的名称以 is 开头,getter不会增加任何的前缀,而它的setter名称中的is会被替换成set。
class Person(
// 只读属性,生成一个字段和一个简单的getter
val name: String,
// 可写属性,一个字段,一个getter和一个setter
var address: String,
var isMarried: Boolean
)
// kotlin转换为java
public final class Person {
@NotNull
private final String name;
@NotNull
private String address;
private boolean isMarried;
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final String getAddress() {
return this.address;
}
public final boolean isMarried() {
return this.isMarried;
}
public final void setMarried(boolean var1) {
this.isMarried = var1;
}
public final void setAddress(@NotNull String var1) {
this.address = var1;
}
public Person(@NotNull String name, @NotNull String address, boolean isMarried) {
this.name = name;
this.address = address;
this.isMarried = isMarried;
}
}
在java 和 kotlin 中调用Person类区别:
// java中使用Person
Person person = new Person("kerwin", "anqing", false);
person.setMarried(true);
System.out.println(person.getName() + " : " + person.getAddress() + " : " + person.isMarried());
// kerwin : anqing : true
// kotlin中使用Person
val person = Person("kerwin", "anqing", false)
person.isMarried = true
println("${person.name} : ${person.address} : ${person.isMarried}")
// kerwin : anqing : true
Kotlin 属性访问器自定义
下面自定义一个属性isSquare,实现getter,它的值是每次访问属性的时计算出来的。
// kotlin
class Rectangle(
val height: Int,
val width: Int
) {
// 自定义访问器
val isSquare: Boolean
// 声明属性的getter
// 也可以这样写: get() = width == height
get() {
return width == height
}
}
// kotlin转java
public final class Rectangle {
private final int height;
private final int width;
public final boolean isSquare() {
return this.width == this.height;
}
public final int getHeight() {
return this.height;
}
public final int getWidth() {
return this.width;
}
public Rectangle(int height, int width) {
this.height = height;
this.width = width;
}
}
Kotlin 目录和包
每一个kotlin文件都能以一条package语句开头,而文件中定义的所有的声明(类、函数、属性)都会被放到这个包中。如果包不相同,则需要导入它们,使用关键字import
kotlin不区分导入的是类还是函数,且允许使用import关键字导入任何种类的声明。可直接导入顶层函数的名称
// 包声明
package com.kerwin.kotlin.demo
class Rectangle(
val height: Int,
val width: Int
) {
val isSquare: Boolean
get() = width == height
}
// 在com.kerwin.kotlin.demo包中定义函数
fun createRectangle(): Rectangle {
return Rectangle(12, 23)
}
导入其他包中的函数
// 包声明
package com.kerwin.kotlin.demo1
// 导入函数名称
import com.kerwin.kotlin.demo.createRectangle
fun main(args: Array<String>) {
println(createRectangle().isSquare)
}
在kotlin中,可以把多个类放在同一个文件中,文件的名字还可以随意选择。kotlin也没有对磁盘上源文件的布局强加任何限制。包层级结构不需要遵循目录层级结构。
Kotlin 程序结构
1.选择处理
声明枚举类
kotlin声明一个枚举类,使用 enum class 关键字。枚举类中定义任何方法,就要使用分号(;)把枚举常量列表和方法定义分开。
// 声明一个带有属性的枚举类
enum class Color(
// 声明枚举常量的属性
val r: Int, val g: Int, val b: Int
) {
// 在每个常量创建时指定属性值
RED(255, 0, 0), ORANGE(255, 165, 0),
YELLOW(255, 255, 0), GREEN(0, 255, 0),
BLUE(0, 0, 255); // 必须要有分号
// 给枚举类定义一个方法
fun rgb() = (r * 256 + g) * 256 + b
}
使用"when"处理枚举类
when 是一个有返回值的表达式。
// kotlin中不需要在每个分支都写上break语句。如果匹配成功。只有对应的分支会执行
fun getColorString(color: Color) = when(color) {
Color.RED -> "red"
Color.BLUE -> "blue"
Color.ORANGE -> "orange"
Color.YELLOW -> "yellow"
Color.GREEN -> "green"
}
可以把多个值合并到同一个分支,只需要用逗号(,)隔开这些值就可以
fun getWarmth(color: Color) = when(color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.BLUE, Color.GREEN -> "cold"
}
在"when"结构中使用任意对象
when 允许使用任何对象作为分支条件。
// 在when分支中使用不同的对象
fun mix(c1: Color, c2: Color) =
// when 表达式的实参可以是任何对象,它被检查是否与分支条件相等
when (setOf(c1, c2)) {
setOf(Color.RED, Color.YELLOW) -> Color.ORANGE
setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN
else -> throw Exception("dirty color")
}
使用不带参数的"when"
如果没有给when表达式提供参数,分支条件就是任意的布尔表达式.
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == Color.RED && c2 == Color.YELLOW) ||
(c1 == Color.YELLOW && c2 == Color.RED) -> Color.ORANGE
(c1 == Color.YELLOW && c2 == Color.BLUE) ||
(c1 == Color.BLUE && c2 == Color.YELLOW) -> Color.GREEN
else -> throw Exception("dirty color")
}
智能转换:合并类型检查和转换
定义一个函数:(1 + 2) + 4 算术表达式求值。智能转换只在变量经过is检查后不再发生变化的情况下有效。属性必须是一个val属性,且不能有自定义的访问器。使用as关键字来表示到特定类型的显示转换。
// 仅作为一个标记接口
interface Expr
// 实现接口使用冒号(:),后面跟上接口名称
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
// 使用if层叠对表达式求值
fun eval(e: Expr): Int {
// 使用is检查来判断一个变量是否是某种类型
if (e is Num) {
// 显示的转换成类型Num是多余的
val num = e as Num
return num.value
}
if (e is Sum) {
// 变量e智能转换了类型
return eval(e.left) + eval(e.right)
}
throw IllegalArgumentException("Unknown")
}
>>> println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
重构:用"when"代替"if"
kotlin没有三元运算符,因为if表达式有返回值。如果if分支只有一个表达式,花括号可以省略。如果if分支是一个代码块,代码块中的最后一个表达式会被作为结果返回。
// 使用有返回值的if表达式
fun eval(e: Expr): Int {
if (e is Num) {
e.value
} else if (e is Sum) {
eval(e.left) + eval(e.right)
} else {
throw IllegalArgumentException("Unknown")
}
}
可以使用when代替if层叠
// when允许检查实参值的类型
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
else -> throw IllegalArgumentException("Unknown")
}
代码块作为 "if" 和 "when" 的分支
if 和 when 都可以使用代码块作为分支体,那么代码块中的最后一个表达式就是结果。
fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right
}
else -> throw IllegalArgumentException("Unknown")
}
2.循环
while循环
和java没有区别,有while循环 和 do-while循环
迭代数字:区间和数列
区间:两个值之间的间隔,这两个值通常是数字,一个起始值,一个结束值。使用 .. 运算符表示区间。
kotlin区间是包含的或者闭合的。是包含结束值的。如果不包含结束值,使用 until 函数
val oneToTen = 1..10
// 不包含size
for (x in 0 until size)
等价于
for (x in 0..size - 1)
迭代1到100之间的所有数字
fun fizzBuzz(i: Int) = when {
i % 15 == 0 -> "FizzBuzz "
i % 3 == 0 -> "Fizz "
i % 5 == 0 -> "Buzz "
else -> "$i "
}
>>> for (i in 1..100) {
print(fizzBuzz(i))
}
迭代带步长的100到1的区间
// 100 downTo 1 是递减的数列(步长为-1)
// step 2 把步长的绝对值变成2,但是方向保持不变(步长被设置成了为-2)
for (i in 100 downTo 1 step 2) {
print(fizzBuzz(i))
}
迭代map
初始化map并迭代。..语法可以创建数字区间,也可以创建字符区间。
// 初始化map,使用TreeMap让键排序
val binaryMap = TreeMap<Char, String>()
// 使用字符区间迭代从A到F之间的字符
for (c in 'A'..'F') {
// 字符二进制
val binary = Integer.toBinaryString(c.toInt())
// 简明语法:map[key] = value 设置值;map[key] 读取值
// 这行等价于:binaryMap.put(c, binary)
binaryMap[c] = binary
}
// 迭代map,把键和值赋给两个变量
for ((letter, binary) in binaryMap) {
println("$letter : $binary")
}
可以在迭代集合时,使用当前下标
val list = arrayListOf("10", "11", "12")
// 迭代集合时使用下标
for ((index, element) in list.withIndex()) {
println("$index : $element")
}
使用 "in" 检查集合和区间的成员
关键字 in 可以迭代区间或者集合,还可以用来检查区间或者集合是否包含了某个值。!in 检查一个值是否不在区间中。
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
// !in 检查是否不在区间中
fun isNotDigit(c: Char) = c !in '0'..'9'
in 运算符 和 !in 检查可以作为 when分支
fun recognize(c: Char) = when (c) {
// 检查值是否在0到9的区间之内
in '0'..'9' -> "It's a digit."
in 'a'..'z', in 'A'..'Z' -> "It's a letter."
else -> "I don't know."
}
Kotlin 函数
1.在kotlin中创建集合
// 创建set
val set = hashSetOf(1, 3, 5)
// kotlin的javaClass等价于java的getClass
println(set.javaClass)
// class java.util.HashSet
// 创建list
val list = arrayListOf(1, 3, 5)
println(list.javaClass)
// class java.util.ArrayList
// 创建map
// to 并不是一个特殊的结构,而是一个普通函数
val map = hashMapOf(1 to "one", 3 to "three", 5 to "five")
println(map.javaClass)
// class java.util.HashMap
kotlin中可以通过下面方式获取一个列表中最后一个元素,或者得到一个数字列表的最大值
val list = listOf("abc", "def", "ker")
println(list.last()) // ker
val set = setOf(1, 23, 4)
println(set.max()) // 23
2.函数声明
java集合都有一个默认的toString方法实现。假设不想使用默认的实现,需要自己实现或者使用第三方库,如:guava 和Apache Commons。
// 自己实现joinToString函数
fun <T> joinToString(
collection: Collection<T>,
prefix: String,
postfix: String,
separator: String
): String {
val result = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {
if (index > 0) result.append(separator).append(" ")
result.append(element)
}
result.append(postfix)
return result.toString()
}
>>> val list = listOf("abc", "def", "ker")
>>> println(joinToString(list, "(", ")", ";"))
(abc; def; ker)
命名参数
// 这种方式不能知道每个参数对应什么含义
joinToString(list, "(", ")", ";")
// kotlin,可以显式的标明一些参数名称
// 如果在调用一个函数时,指明了一个参数名称,为了避免混淆,那它之后所有参数都需要标明名称
joinToString(list, prefix = "(", postfix = ")", separator = ";")
默认参数值
java中存在一个普遍的问题,一些类的重载函数很多,如:Thread类(8个构造方法)。
在kotlin中,可以在声明函数的时候,指定参数的默认值,就可以避免创建重载的函数。
fun <T> joinToString(
collection: Collection<T>,
prefix: String = "", // 有默认的参数
postfix: String = "",
separator: String = ", "
): String
// 调用的时候,可以省略只有排在末尾的参数
>>> joinToString(list)
>>> joinToString(list, "(", ")")
// 如果使用命名参数,可以省略中间的一些参数
>>> joinToString(list, separator = "; ")
// 也可以以任意顺序只给定需要的参数
>>> joinToString(list, separator = "; ", prefix = "[")
消除静态工具类:顶层函数和属性
在kotlin中,不需要创建静态工具类,可以把函数直接放到代码文件的顶层,不用从属任何的类。
声明joinToString作为顶层函数
// join.kt
package strings
fun joinToString(...): String { ... }
// 当编译join.kt这个文件时,会生成一些类,JoinKt.java。因为JVM只能执行类中的代码。
// 且join.kt文件中的所有顶层函数编译为JoinKt.java这个类的静态函数
// 从java中调用这些函数:JoinKt.joinToString(list, "", "", "");
public final class JoinKt {
@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String prefix, @NotNull String postfix, @NotNull String separator) {
...
}
}
修改文件类名:
要修改包含kotlin顶层函数的生成的类的名称,需要为这个文件添加 @file:JvmName 注解,将其放到这个文件的开头,位于包名的前面
// 注解指定类名
@file:JvmName("StringUtils")
// 包的声明跟在文件注解后
package strings
fun <T> joinToString(
collection: Collection<T>,
prefix: String = "",
postfix: String = "",
separator: String = ", "
): String { ... }
// 编译后,生成StringUtils.class文件
// 在java代码中调用这个函数:StringUtils.joinToString(list, "", "", "")
顶层属性和函数一样,属性也可以放到文件的顶层。
package strings
val LINE_SEPARATOR = "\n"
// 编译后,私有的静态常量。
private static final String LINE_SEPARATOR = "\n";
// 使用const修饰,const val LINE_SEPARATOR = "\n" 编译后生成public的静态常量
public static final String LINE_SEPARATOR = "\n";
3.扩展函数和属性
扩展函数就是把要扩展的类或者接口的名称,放到即将添加的函数前面。这个类的名称称为接受者类型,用来调用这个扩展函数的那个对象,叫作接受者对象。但是扩展函数不能访问私有的或者受保护的成员。
package strings
// 为String类添加自己的方法:字符串的最后一个字符
// String:接受者类型 this:接受者对象
fun String.lastChar(): Char = this[this.length - 1]
>>> println("abc".lastChar())
导入和扩展函数
kotlin允许用和导入类一样的语法来导入单个函数。
import strings.lastChar
// 使用 * 来导入也可以: import strings.*
// 还可以使用关键字 as 来修改导入的类或者函数名称
// import strings.lastChar as last
// 调用: val lastChar = "abc".last()
val lastChar = "abc".lastChar()
从java中调用扩展函数
扩展函数就是静态函数,它把调用对象作为了它的第一个参数。
// 把接受者对象作为第一个参数传进去即可
StringUtils.lastChar("abc")
作为扩展函数的工具函数
为元素的集合类Collection添加一个扩展函数,然后给所有的参数添加一个默认值。
@file:JvmName("StringUtils")
package strings
import java.lang.StringBuilder
// Collection<T>: 接受者类型
// 为Collection<T> 声明一个扩展函数
fun <T> Collection<T>.joinToString(
prefix: String = "",
postfix: String = "",
separator: String = ", "
): String {
val result = StringBuilder(prefix)
// this: 接受者对象
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator).append(" ")
result.append(element)
}
result.append(postfix)
return result.toString()
}
>>> val list = listOf("abc", "def", "ker")
>>> println(list.joinToString(prefix = "{", postfix = "}", separator = "; "))
{abc; def; ker}
不可重写的扩展函数
kotlin中不能重写扩展函数,因为kotlin会把它们当作静态函数对待。
扩展属性
扩展属性必须定义getter函数,因为没有支持字段,因此没有默认getter实现,初始化也不可以,因为没有地方存储初始值。
// 声明一个扩展属性
val String.lastChar: Char
get() = this[this.length - 1]
也可以声明一个可变的扩展属性
var StringBuilder.lastChar: Char
// getter属性
get() = this.get(this.length - 1)
// setter属性
set(value) {
this.setCharAt(length - 1, value)
}
>>> val sb = StringBuilder("ab?")
>>> sb.lastChar = '!'
>>> println(sb) // ab!
// 如果从java中访问扩展属性,显式的调用它的getter函数
StringUtils.getLastChar(new StringBuilder("abc"));
4.处理集合:可变参数、中缀调用和库的支持
- 可变参数的关键字 vararg,声明一个函数可以接收任意数量的参数
- 一个中缀表达法,当调用一些只有一个参数的函数时,可让代码更简练
- 解构声明,用来把一个单独的组合值展开到多个变量中
扩展java集合的API
val list = listOf("abc", "def", "ker")
// last函数被定义为List的扩展函数
println(list.last()) // ker
// public fun <T> List<T>.last(): T { ... }
可变参数:让函数支持任意数量的参数
kotlin中,在参数上使用vararg修饰符;java中使用三个点(...)
val list = listOf("abc", "def", "ker")
// listOf函数在库中声明
public fun <T> listOf(vararg elements: T): List<T>
还有一个区别:当需要传递的参数是已经包装在数组中时,在java中,可以按原样传递数组;kotlin中要求显式的解包数组,以便每个数组元素在函数中能作为单独的参数来调用。
val array = arrayOf(11, 12, 13)
// 使用展开运算符(*)传递数组
val list = listOf("10", *array)
>>> println(list) // [10, 11, 12, 13]
键值对处理:中缀调用和解构声明
val map = mapOf(1 to "one", 2 to "two")
// mapOf函数在库中的声明
public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>
// 一般函数调用: 2.to("two"); 中缀符号调用to函数:2 to "two"
// to 不是内置结构,而是一种特殊的函数调用,称为中缀调用
// to 函数在库中的声明
// 中缀调用可以与只有一个参数的函数一起使用,使用中缀符号调用函数,需要使用**infix**修饰符标记。
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
可以直接用Pair的内容来初始化两个变量,称为解构声明。解构声明还可以使用map的key和value内容来初始化两个变量。也适用于循环,如: withIndex
val (number, name) = 1 to "one"
val list = listOf("abc", "def", "ker")
for ((index, element) in list.withIndex()) {
println("$index : $element")
}
5.字符串和正则表达式的处理
字符串模板
kotlin可以在字符串字面值中引用局部变量,只需要在变量名称前面加上字符$
val x = 12
println("x = $x") // x = 12
// 如果对它转义,不会把x解析成变量的引用
println("x = \$x") //x = $x
// 如果引用复杂的表达式,需要把表达式用花括号括起来
val x= 1
val y = 2
println("x + y = ${x + y}")
分割字符串
// 在java中,期望得到[12, 345-6, A],但是返回是一个空数组,因为点号(.)表示任何字符的正则表达式
System.out.println(Arrays.toString("12.345-6.A".split("."))); // []
// 在kotlin中,可以显示的创建一个正则表达式分割字符串
// 需要转义表示字面量,而不是通配符
// 使用扩展函数toRegex将字符串转换为正则表达式
println("12.345-6.A".split("\\.".toRegex())) // [12, 345-6, A]
// kotlin中的split扩展函数的其他重载支持任意数量的纯文本字符串分隔符;
// 指定多个分隔符
println("12.345-6.A".split(".", "-")) // [12, 345, 6, A]
正则表达式和三重引号字符串
使用String的扩展函数来解析文件路径。
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, fileName: $fileName, ext: $extension")
//Dir: /User/kerwin/book, fileName: readme, ext: md
}
>>> parsePath("/User/kerwin/book/readme.md")
使用正则表达式解析文件路径
fun parsePath(path: String) {
// 正则表达式写在一个 三重引号的字符串中,不需要对任何字符进行转义,包括反斜线,所以可以用\.而不是\\.表示点
// (.+) 目录,/ 最后一个斜线,(.+) 文件名,\. 最后一个点,(.+) 扩展名
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
val (directory, fileName, extension) = matchResult.destructured
println("Dir: $directory, fileName: $fileName, ext: $extension")
}
}
多行三重引号的字符串
可以避免转义字符,且可以包含任何字符,包括换行符,用于格式化代码的缩进。
val string = """| //
.|//
.|/ \ """
// 可以去掉缩进
// 先向字符串内容添加前缀,标记边距的结尾,然后调用trimMargin来删除每行中的前缀和前面的空格
>>> println(string.trimMargin("."))
| //
|//
|/ \
6. 局部函数和扩展
带重复代码的函数
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}, name is empty.")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}, address is empty.")
}
// save user ...
}
提取局部函数来避免重复,在布局函数中可以访问外层函数的参数
fun saveUser(user: User) {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}, $fieldName is empty.")
}
}
validate(user.name, "name")
validate(user.address, "address")
// save user ...
}
提取逻辑到扩展函数
class User(val id: Int, val name: String, val address: String)
fun User.validateBeforeSave(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${this.id}, $fieldName is empty.")
}
}
fun saveUser(user: User) {
user.validateBeforeSave(user.name, "name")
user.validateBeforeSave(user.address, "address")
// save user ...
}
如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。