导图社区 kotlin-类与对象
将kotlin 类与对象的概念与其中的重要部分记录下来,方便以后查阅,复习知识点
编辑于2020-03-10 07:24:53类与对象
1 类Class
2 构造函数
主构造函数
主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。 class Person constructor(firstName: String) { /*……*/ } 如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字。 class Person(firstName: String) { /*……*/ } 主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中。
次构造函数
类也可以声明前缀有 constructor的次构造函数: class Person { var children: MutableList<Person> = mutableListOf<Person>(); constructor(parent: Person) { parent.children.add(this) } } 如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可: class Person(val name: String) { var children: MutableList<Person> = mutableListOf<Person>(); constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } }
3 创建类的实例
注意 Kotlin 并没有 new 关键字。
val invoice = Invoice() val customer = Customer("Joe Smith")
4 类成员
构造函数与初始化块 函数 属性 嵌套类与内部类 对象声明
5 继承(open abstract interface)
在 Kotlin 中所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类 Any 有三个方法:equals()、 hashCode() 与 toString()。因此,为所有 Kotlin 类都定义了这些方法。 如需声明一个显式的超类型,请在类头中把超类型放到冒号之后: open class Base(p: Int) class Derived(p: Int) : Base(p) 如果派生类有一个主构造函数,其基类可以(并且必须) 用派生类主构造函数的参数就地初始化。
覆盖方法(Open -- override)
标记为 override 的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,使用 final 关键字
覆盖属性
在超类中声明然后在派生类中重新声明的属性必须以 override 开头,并且它们必须具有兼容的类型。 每个声明的属性可以由具有初始化器的属性或者具有 get 方法的属性覆盖 你也可以用一个 var 属性覆盖一个 val 属性,但反之则不行。 这是允许的,因为一个 val 属性本质上声明了一个 get 方法, 而将其覆盖为 var 只是在子类中额外声明一个 set 方法。
派生类初始化顺序
调用超类实现
派生类中的代码可以使用 super 关键字调用其超类的函数与属性访问器的实现
覆盖规则
如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。 为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super,如 super<Base>
抽象类(abstract)
伴生对象
类内部的对象声明可以用 companion 关键字标记
6 类属性(var,val)
Kotlin 类中的属性既可以用关键字 var 声明为可变的,也可以用关键字 val 声明为只读的。
声明属性
getters 与 setters
声明一个属性的完整语法是 var <propertyName>[: <PropertyType>] [= <property_initializer>] [<getter>] [<setter>] 其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器 (或者从其 getter 返回值,如下文所示)中推断出来,也可以省略。
幕后字段
幕后属性
编译期常量(const)
位于顶层或者是 object 声明 或 companion object 的一个成员 以 String 或原生类型值初始化 没有自定义 getter
延迟初始化属性与变量
覆盖属性
委托属性
有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好。例如包括: 延迟属性(lazy properties): 其值只在首次访问时计算; 可观察属性(observable properties): 监听器会收到有关此属性变更的通知; 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。 为了涵盖这些(以及其他)情况,Kotlin 支持 委托属性: class Example { var p: String by Delegate() }
语法是: val/var <属性名>: <类型> by <表达式>。
标准委托
7 接口 interface
Kotlin 的接口可以既包含抽象方法的声明也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。 使用关键字 interface 来定义接口 interface MyInterface { fun bar() fun foo() { // 可选的方法体 } }
实现接口
接口中的属性
接口继承
解决覆盖冲突
interface A { fun foo() { print("A") } fun bar() } interface B { fun foo() { print("B") } fun bar() { print("bar") } } class C : A { override fun bar() { print("bar") } } class D : A, B { override fun foo() { super<A>.foo() super<B>.foo() } override fun bar() { super<B>.bar() } }
8 可见性修饰符
private、 protected、 internal 和 public(默认)
包
如果你不指定任何可见性修饰符,默认为 public,这意味着你的声明将随处可见; 如果你声明为 private,它只会在声明它的文件内可见; 如果你声明为 internal,它会在相同模块内随处可见; protected 不适用于顶层声明。
类和接口
private 意味着只在这个类内部(包含其所有成员)可见; protected—— 和 private一样 + 在子类中可见。 internal —— 能见到类声明的 本模块内 的任何客户端都可见其 internal 成员; public —— 能见到类声明的任何客户端都可见其 public 成员。
构造函数
默认情况下,所有构造函数都是 public,这实际上等于类可见的地方它就可见(即 一个 internal 类的构造函数只能在相同模块内可见).
局部声明
局部变量、函数和类不能有可见性修饰符。
模块
可见性修饰符 internal 意味着该成员只在相同模块内可见。更具体地说, 一个模块是编译在一起的一套 Kotlin 文件: 一个 IntelliJ IDEA 模块; 一个 Maven 项目; 一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明); 一次 <kotlinc> Ant 任务执行所编译的一套文件。
9 扩展
Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做 扩展 的特殊声明完成
扩展函数
声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀
扩展是静态解析的
可空接收者
扩展属性
注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义。 例如: val House.number = 1 // 错误:扩展属性不能有初始化器 val House.number = 1 // 错误:扩展属性不能有初始化器
伴生对象的扩展
扩展的作用域
大多数时候我们在顶层定义扩展——直接在包里
扩展声明为成员
在一个类内部你可以为另一个类声明扩展。在这样的扩展内部,有多个 隐式接收者 —— 其中的对象成员可以无需通过限定符访问。扩展声明所在的类的实例称为 分发接收者,扩展方法调用所在的接收者类型的实例称为 扩展接收者 。 class Host(val hostname: String) { fun printHostname() { print(hostname) } } class Connection(val host: Host, val port: Int) { fun printPort() { print(port) } fun Host.printConnectionString() { printHostname() // 调用 Host.printHostname() print(":") printPort() // 调用 Connection.printPort() } fun connect() { /*……*/ host.printConnectionString() // 调用扩展函数 } } fun main() { Connection(Host("kotl.in"), 443).connect() //Host("kotl.in").printConnectionString(443) // 错误,该扩展函数在 Connection 外不可用 }
关于可见性的说明
扩展的可见性与相同作用域内声明的其他实体的可见性相同。例如: 在文件顶层声明的扩展可以访问同一文件中的其他 private 顶层声明; 如果扩展是在其接收者类型外部声明的,那么该扩展不能访问接收者的 private 成员。
19 委托属性
延迟属性(lazy properties): 其值只在首次访问时计算; 可观察属性(observable properties): 监听器会收到有关此属性变更的通知; 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。
val/var <属性名>: <类型> by <表达式>
自定义委托
import kotlin.reflect.KProperty 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.") } } class Example { var p: String by Delegate() } fun main(){ val e = Example() e.p = "hello" // 会调用 Delegate setValue 方法 println(e.p) // 会调用 Delegate getValue 方法 }
标准委托
延迟属性 lazy
lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。 val lazyValue: String by lazy { println("computed!") "Hello" } fun main() { println(lazyValue) // 会调用 println("computed!") println(lazyValue) // 不会调用 由于已经赋值过 会直接打印结果 } 默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 而如果你确定初始化将总是发生在与属性使用位于相同的线程, 那么可以使用 LazyThreadSafetyMode.NONE 模式:它不会有任何线程安全的保证以及相关的开销。
可观察属性 Observable
可观察且否定属性 vetoable
把属性储存在映射中
一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。 class User(val map: Map<String, Any?>) { val name: String by map val age: Int by map } 在这个例子中,构造函数接受一个映射参数: val user = User(mapOf( "name" to "John Doe", "age" to 25 ))
局部委托属性
属性委托要求
18 委托
委托实现
委托模式已经证明是实现继承的一个很好的替代方式, 而 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() { val b = BaseImpl(10) Derived(b).print() } Target platform: JVMRunning on kotlin v. 1.3.70 Derived 的超类型列表中的 by-子句表示 b 将会在 Derived 中内部存储, 并且编译器将生成转发给 b 的所有 Base 的方法。
覆盖由委托实现的接口成员
覆盖符合预期:编译器会使用 override 覆盖的实现而不是委托对象中的。如果将 override fun printMessage() { print("abc") } 添加到 Derived,那么当调用 printMessage 时程序会输出“abc”而不是“10”: interface Base { fun printMessage() fun printMessageLine() } class BaseImpl(val x: Int) : Base { override fun printMessage() { print(x) } override fun printMessageLine() { println(x) } } class Derived(b: Base) : Base by b { override fun printMessage() { print("abc") } } fun main() { val b = BaseImpl(10) Derived(b).printMessage() Derived(b).printMessageLine() }
17 内联类
未商用
16 类型别名 typealias
15 对象
有时候,我们需要创建一个对某个类做了轻微改动的类的对象,而不用为之显式声明新的子类。 Kotlin 用对象表达式和对象声明处理这种情况。
对象表达式 object
可使用object 表示匿名内部类
可用val xx= object{}直接创建临时对象
对象声明
单例模式
object xxx{} 线程安全
伴生对象 companion
用来对应java中的static描述的函数或对象
使用 @JvmStatic 可在编译后成为真实的java静态函数或对象
对象表达式和对象声明之间的语义差异
对象表达式和对象声明之间有一个重要的语义差别: 对象表达式是在使用他们的地方立即执行(及初始化)的; 对象声明是在第一次被访问到时延迟初始化的; 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配
14 枚举类 enum
初始化
enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) }
匿名类
枚举常量还可以声明其带有相应方法以及覆盖了基类方法的匿名类。 enum class ProtocolState { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = WAITING }; abstract fun signal(): ProtocolState } 如果枚举类定义任何成员,那么使用分号将成员定义中的枚举常量定义分隔开。 枚举条目不能包含内部类以外的嵌套类型(已在 Kotlin 1.2 中弃用)。
在枚举类中实现接口
一个枚举类可以实现接口(但不能从类继承),可以为所有条目提供统一的接口成员实现,也可以在相应匿名类中为每个条目提供各自的实现。只需将接口添加到枚举类声明中即可,如下所示: enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator { PLUS { override fun apply(t: Int, u: Int): Int = t + u }, TIMES { override fun apply(t: Int, u: Int): Int = t * u }; override fun applyAsInt(t: Int, u: Int) = apply(t, u) }
使用枚举常量
13 嵌套类和内部类
嵌套类
类可以嵌套在其他类中: class Outer { private val bar: Int = 1 class Nested { fun foo() = 2 } } val demo = Outer.Nested().foo() // == 2 class Outer { private val bar: Int = 1 class Nested { fun foo() = 2 } } val demo = Outer.Nested().foo() // == 2
内部类 inner
匿名内部类
使用对象表达式创建
12 泛型 <T>
父类泛型对象可以赋值给子类泛型对象,用 in; 子类泛型对象可以赋值给父类泛型对象,用 out。
型变
声明处型变 out T
假设有一个泛型接口 Source<T>,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值: // Java interface Source<T> { T nextT(); } 那么,在 Source <Object> 类型的变量中存储 Source <String> 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作: // Java void demo(Source<String> strs) { Source<Object> objects = strs; // !!!在 Java 中不允许 // …… } 为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。 在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source 的类型参数 T 来确保它仅从 Source<T> 成员中返回(生产),并从不被消费。 为此,我们提供 out 修饰符
out T 相当于 java 的 ? extends Object
var a :List<out T> 定义之后将不可调用 设置方法
逆变 in T
有点类似 java 的 ? super T
类型投影
星投影
有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。
泛型函数
类型参数要放在函数名称之前: fun <T> singletonList(item: T): List<T> { // …… } fun <T> T.basicToString(): String { // 扩展函数 // …… } 要调用泛型函数,在调用处函数名之后指定类型参数即可: val l = singletonList<Int>(1) 可以省略能够从上下文中推断出来的类型参数,所以以下示例同样适用: val l = singletonList(1)
泛型约束
上界 类似java的extends对应的上界 使用:指定 且可以使用where指定条件
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String> where T : CharSequence, T : Comparable<T> { return list.filter { it > threshold }.map { it.toString() } } 所传递的类型必须同时满足 where 子句的所有条件。在上述示例中,类型 T 必须既实现了 CharSequence 也实现了 Comparable。
类型擦除
11 密封类 sealed class
密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。 sealed class Expr data class Const(val number: Double) : Expr() data class Sum(val e1: Expr, val e2: Expr) : Expr() object NotANumber : Expr() 一个密封类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员。 密封类不允许有非-private 构造函数(其构造函数默认为 private)。 请注意,扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。 使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。当然,这只有当你用 when 作为表达式(使用结果)而不是作为语句时才有用。 fun eval(expr: Expr): Double = when(expr) { is Const -> expr.number is Sum -> eval(expr.e1) + eval(expr.e2) NotANumber -> Double.NaN // 不再需要 `else` 子句,因为我们已经覆盖了所有的情况 }
10 数据类 data class
编译器自动从主构造函数中声明的所有属性导出以下成员: equals()/hashCode() 对; toString() 格式是 "User(name=John, age=42)"; componentN() 函数 按声明顺序对应于所有属性; copy() 函数(见下文)。 为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求: 主构造函数需要至少有一个参数; 主构造函数的所有参数需要标记为 val 或 var; 数据类不能是抽象、开放、密封或者内部的; (在1.1之前)数据类只能实现接口。 此外,成员生成遵循关于成员继承的这些规则: 如果在数据类体中有显式实现 equals()、 hashCode() 或者 toString(),或者这些函数在父类中有 final 实现,那么不会生成这些函数,而会使用现有函数; 如果超类型具有 open 的 componentN() 函数并且返回兼容的类型, 那么会为数据类生成相应的函数,并覆盖超类的实现。如果超类型的这些函数由于签名不兼容或者是 final 而导致无法覆盖,那么会报错; 从一个已具 copy(……) 函数且签名匹配的类型派生一个数据类在 Kotlin 1.2 中已弃用,并且在 Kotlin 1.3 中已禁用。 不允许为 componentN() 以及 copy() 函数提供显式实现。 自 1.1 起,数据类可以扩展其他类(示例请参见密封类)。 在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值。 (参见构造函数)。 data class User(val name: String = "", val age: Int = 0)
我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data
data class User(val name: String, val age: Int)
在类体中声明的属性
复制 copy
数据类与解构声明
标准数据类