Java其实不是编译型语言,java编译之后不是计算机可识别的二进制文件,而是一种特殊的class文件,这种文件只有在Java虚拟机(Android是ART)才能识别,这个虚拟机才是编译器的角色,所以 java是解释型语言。所以Kotlin只要编译成同样规格的class文件,就行了。
Kotlin几乎杜绝了空指针异常。
一、类和继承
1、类
- 如果一个类没有实体,可以省略花括号:class Empty
2、构造函数
-
在 Kotlin 中的一个类可以有一个主构造函数和一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(和可选的类型参数)后:
class Person constructor(firstName: String) { }
-
如果主构造函数没有任何注解或者可见性修饰符,可以省略这个constructor关键字:
class Person(firstName: String) { }
-
主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中:
class Customer(name: String) { init { logger.info("Customer initialized with value ${name}") } }
-
注意,主构造的参数可以在初始化块中使用。它们也可以在类体内声明的属性初始化器中使用:
class Customer(name: String) { val customerKey = name.toUpperCase() }
-
事实上,声明属性以及从主构造函数初始化属性,Kotlin 有简洁的语法:
class Person(val firstName: String, val lastName: String, var age: Int) { // …… }
与普通属性一样,主构造函数中声明的属性可以是可变的(var)或只读的(val)。
-
如果构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面:
class Customer public @Inject constructor(name: String) { …… }
3、次构造函数
-
前缀为constructor
class Person { constructor(parent: Person) { parent.children.add(this) } }
-
如果类有一个主构造函数,每个次构造函数需要委托给主构造函数,可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用this关键字即可:
class Person(val name: String) { constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } }
-
如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数:
class DontCreateMe private constructor () { }
-
参数可以直接在构造函数初始化:
class Customer(val customerName: String = "")
4、实例
-
Kotlin并没有new关键字,要创建一个类的实例,我们就像普通函数一样调用构造数
val invoice = Invoice() val customer = Customer("Joe Smith")
5、继承
在Kotlin中所有类都有一个共同的超类Any,这对于没有超类型声明的类是默认超类:
class Example // 从Any隐式继承-
继承用冒号:
open class Base(p: Int) class Derived(p: Int) : Base(p)
类上的open标注这个类才可以被继承,默认情况下Kotlin中所有的类都是 final。
-
如果类没有主构造函数,那么每个次构造函数必须使用 super 关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
class MyView : View { constructor(ctx: Context) : super(ctx) constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) }
6、覆盖方法
-
Kotlin 需要显式标注可覆盖的成员(我们称之为开放)和覆盖后的成员:
open class Base { open fun v() {} fun nv() {} } class Derived() : Base() { override fun v() {} }
可覆盖的成员必须用open修饰,覆盖后用override修饰。
-
标记为override的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,使用final关键字:
open class AnotherDerived() : Base() { final override fun v() {} }
7、覆盖属性
-
属性覆盖与方法覆盖类似,每个声明的属性可以由具有初始化器的属性或者具有getter 方法的属性覆盖。
open class Foo { open val x: Int get { …… } } class Bar1 : Foo() { override val x: Int = …… }
你也可以用一个 var 属性覆盖一个 val 属性,但反之则不行。这是允许的,因为一个val 属性本质上声明了一个 getter 方法,而将其覆盖为 var 只是在子类中额外声明一个setter 方法。
8、覆盖规则
-
在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。 为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super ,如 super
: open class A { open fun f() { print("A") } fun a() { print("a") } } interface B { fun f() { print("B") } // 接口成员默认就是“open”的 fun b() { print("b") } } class C() : A(), B { // 编译器要求覆盖 f(): override fun f() { super.f() // 调用 A.f() super.f() // 调用 B.f() } }
二、属性和字段
1、声明属性
变量用var,常量用val。
-
延迟初始化属性:
一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检查。
为处理这种情况,你可以用 lateinit 修饰符标记该属性:public class MyTest { lateinit var subject: TestSubject @SetUp fun setup() { subject = TestSubject() } @Test fun test() { subject.method() // 直接解引用 } }
该修饰符只能用于在类体中(不是在主构造函数中)声明的 var 属性,并且仅当该属性没有自定义 getter 或 setter 时。该属性必须是非空类型,并且不能是原生类型。在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。
三、可见性修饰符
1、private
- 意味着只在这个类内部(包含其所有成员)可见。
2、protected - 和 private 一样 + 在子类中可见。
3、Internal - 能见到类声明的本模块内的任何客户端都可见其internal成员,一个模块是编译在一起的一套 Kotlin 文件。
4、public - 能见到类声明的任何客户端都可见其 public 成员。
四、扩展
1、扩展函数
-
fun 要扩展的类. 函数名[(参数1:类型,参数2:类型)]{}
代码演示:在Int类扩展sum函数后,任何Int类型的对象都可以调用该函数fun Int.sum(a:Int,b:Int) = a + b 然后调用:2.sum(1,2)
需要注意的是,扩展不能真正的修改他们所扩展的类,通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数。
2、扩展属性
-
扩展属性需要注意点就是:扩展的属性不像原有属性一样自带 get()、set(),需要自己声明,而且必须声明。
例如:给C类分别添加a,b,c属性。 a属性正常,b属性要么更改为val ,要么添加set()函数。c必须添加get()函数。val C.a : Int //正常 get() = 1 var C.b : Int //必须声明为val get() = 1 val C.c : Int //必须定义get()
3、扩展声明为成员
-
在一个类内部你可以为另一个类声明扩展。在这样的扩展内部,有多个隐式接收者——其中的对象成员可以无需通过限定符访问。扩展声明所在的类的实例称为分发接收者,扩展方法调用所在的接收者类型的实例称为扩展接收者。
class D { fun bar() { …… } } class C { fun baz() { …… } fun D.foo() { bar() // 调用 D.bar baz() // 调用 C.baz } fun caller(d: D) { d.foo() // 调用扩展函数 }
-
对于分发接收者和扩展接收者的成员名字冲突的情况,扩展接收者优先。要引用分发接收者的成员你可以使用限定的this语法。
class C { fun D.foo() { toString() // 调用 D.toString() [email protected]() // 调用 C.toString() } }
五、数据类
1、数据类
-
我们经常创建一些只保存数据的类。在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data :
data class User(val name: String, val age: Int) data class User(val name: String = "", val age: Int = 0) //指定默认值
2、复制
-
在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 copy()函数就是为此而生成。对于上文的 User 类,其实现会类似下面这样:
val jack = User(name = "Jack", age = 1) val olderJack = jack.copy(age = 2)
3、数据类和解构声明
-
为数据类生成的 Component 函数 使它们可在解构声明中使用:
val jane = User("Jane", 35) val (name, age) = jane println("$name, $age years of age") // 输出 "Jane, 35 years of age"
解构声明就是把一个对象解构成很多变量
六、密封类
1、密封类
密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
-
要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。
sealed class Expr { data class Const(val number: Double) : Expr() data class Sum(val e1: Expr, val e2: Expr) : Expr() object NotANumber : Expr() }
-
使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个else子句了。
fun eval(expr: Expr): Double = when(expr) { is Const -> expr.number is Sum -> eval(expr.e1) + eval(expr.e2) NotANumber -> Double.NaN // 不再需要 `else` 子句,因为我们已经覆盖了所有的情况 }
七、嵌套类
1、嵌套类
-
类可以嵌套在其他类中
class Outer { private val bar: Int = 1 class Nested { fun foo() = 2 } } val demo = Outer.Nested().foo() // == 2
2、内部类
-
类可以标记为inner以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用:
class Outer { private val bar: Int = 1 inner class Inner { fun foo() = bar } } val demo = Outer().Inner().foo() // == 1
3、匿名内部类
-
使用对象表达式创建匿名内部类实例:
window.addMouseListener(object: MouseAdapter() { override fun mouseClicked(e: MouseEvent) { // …… } override fun mouseEntered(e: MouseEvent) { // …… } })
八、对象
1、伴生对象
-
与Java或C#不同,Kotli中类没有静态方法,如果你需要写一个可以无需用一个类的实例来调用、但需要访问类内部的函数(例如,工厂方法),你可以把它写成该类内对象声明中的一员。
类内部的对象声明可以用 companion 关键字标记:class MyClass { companion object Factory { fun create(): MyClass = MyClass() } } val instance = MyClass.create()
-
可以省略伴生对象的名称,在这种情况下将使用名称Companion:
class MyClass { companion object { } } val x = MyClass.Companion
即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象
的实例成员
九、委托
1、类委托
-
委托模式已经证明是实现继承的一个很好的替代方式, 而 Kotlin 可以零样板代码地原生支持它。 类Derived可以继承一个接口Base,并将其所有共有的方法委托给一个指定的对象:
interface Base { fun print() } class BaseImpl(val x: Int) : Base { override fun print() { print(x) } } class Derived(b: Base) : Base by b fun main(args: Array
) { val b = BaseImpl(10) Derived(b).print() // 输出 10 } 如果我们为Derived添加override fun print() { print("abc") },该程序会输出“abc”而不是“10”。
2、委托属性
-
语法是:val/var <属性名>: <类型> by <表达式>。在by后面的表达式是该 委托, 因为属性对应的get()(和 set())会被委托给它的getValue()和setValue()方法。 属性的委托不必实现任何的接口,但是需要提供一个getValue()函数(和setValue())——对于var属性)。
class Example { var p: String by Delegate() } class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name} in $thisRef.'") } }
-
当我们从委托到一个Delegate实例的p读取时,将调用Delegate中的getValue()函数, 所以它第一个参数是读出p的对象、第二个参数保存了对p自身的描述 (例如你可以取它的名字)。 例如:
val e = Example() println(e.p) 输出结果: Example@33a17727, thank you for delegating‘p’to me!
3、延迟属性 Lazy
延迟属性(lazy properties): 其值只在首次访问时计算。
-
lazy() 是接受一个lambda并返回一个Lazy
实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用get()会执行已传递给lazy()的lamda表达式并记录结果,后续调用get()只是返回记录的结果: val lazyValue: String by lazy { println("computed!") "Hello" } fun main(args: Array
) { println(lazyValue) println(lazyValue) } 这个例子输出: computed! Hello Hello
4、可观察属性 Observable
可观察属性(observable properties): 监听器会收到有关此属性变更的通知。
-
Delegates.observable() 接受两个参数:初始值和修改时处理程序(handler)。 每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值和新值:
import kotlin.properties.Delegates class User { var name: String by Delegates.observable("
") { prop, old, new -> println("$old -> $new") } } fun main(args: Array ) { val user = User() user.name = "first" user.name = "second" } 这个例子输出: -> first first -> second
十、空安全
1、可空类型与非空类型
Kotlin 的类型系统旨在从我们的代码中消除NullPointerException。
-
在Kotlin中,类型系统区分一个引用可以容纳null(可空引用)还是不能容纳(非空引用)。 例如,String 类型的常规变量不能容纳null:
var a: String = "abc" a = null // 编译错误 如果要允许为空,我们可以声明一个变量为可空字符串,写作String?: var b: String? = "abc" b = null // ok 再看下面的例子: var a: String = "abc" var b: String? = "abc" val l = a.length //正常,应为a不会为空 val l = b.length // 错误:变量“b”可能为空
2、安全的调用
b?.length,如果b非空,就返回b.length,否则返回null。
-
如果要只对非空值执行某个操作,安全调用操作符可以与let一起使用:
val listWithNulls: List
= listOf("A", null) for (item in listWithNulls) { item?.let { println(it) } // 输出 A 并忽略 null }
3、Elvis 操作符
-
当我们有一个可空的引用r时,我们可以说“如果r非空,我使用它;否则使用某个非空的值x”:
val l = b?.length ?: -1 如果?:左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。 请注意,当且仅当左侧为空时,才会对右侧表达式求值。
4、!! 操作符
-
我们可以写b!!,这会返回一个非空的b值 (例如:在我们例子中的String)或者如果 b为空,就会抛出一个NPE异常:
val l = b!!.length
5、安全的类型转换
-
如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException 。 另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null :
val aInt: Int? = a as? Int
6、可空类型的集合
-
如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用 filterNotNull 来实现。
val nullableList: List
= listOf(1, 2, null, 4) val intList: List = nullableList.filterNotNull()