Kotlin学习笔记之类与对象
Kotlin学习笔记之类与对象
类和继承
与java一样,通过关键字class声明类,但是与java不同的地方有很多:
类声明由类名、类头(指定其类型参数、主
构造函数等)和由大括号包围的类体构成。类头和类体都是可选的; 如果一个类没有类体,可以省略花括号。
例如:class Empty
构造函数
在 Kotlin 中的一个类可以有一个 主构造函数
和一个或多个 次构造函数
。主
构造函数是类头的一部分:它跟在类名(和可选的类型参数)后,可见性修饰符必须写在constructor前面。
class Person public @Inject 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 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
(
)
{
}
注意
:在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成 一个额外的无参构造函数,它将使用默认值。这使得 Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库。
class Customer ( val customerName : String= ""
)
创建类的实例
要创建一个类的实例,我们就像普通函数一样调用构造函数,Kotlin中没有new关键字:
val invoice=Invoice (
)
val customer=Customer (
“Joe Smith”
)
继承
在 Kotlin 中所有类都有一个共同的超类
Any
,这对于没有超类型声明的类是默认超类:
class Example // 从 Any 隐式继承
Any不是
java.lang.Object
;尤其是,它除了
equals()
、
hashCode()
和
toString()
外没有任何成员。 要声明一个显式的超类型,我们把类型放到类头的冒号之后:
open class Base ( p : Int )
class Derived ( p : Int )
: Base ( p )
如果该类有一个主构造函数,其基类型可以(并且必须) 用(基类型的)主构造函数参数就地初始化。
如果类没有主构造函数,那么每个次构造函数必须
使用
super
关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
class MyView : View {
constructor
( ctx : Context )
: super
( ctx )
constructor
( ctx : Context , attrs : AttributeSet )
: super
( ctx , attrs )
}
类上的
open
标注与 Java 中
final
相反,它允许其他类
从这个类继承。默认情况下,在 Kotlin 中所有的类都是 final, 对应于
书中的
第 17 条: 要么为继承而设计,并提供文档说明,要么就禁止继承
。
覆盖方法
我们之前提到过,Kotlin 力求清晰显式。与 Java 不同,Kotlin 需要显式
标注可覆盖的成员(我们称之为
开放
)和覆盖后的成员:
open class Base {
open fun v (
)
{
}
fun nv (
)
{
}
}
class Derived (
)
: Base (
)
{
override fun v (
)
{
}
}
Derived.v() 函数上必须加上 override
标注。如果没写,编译器将会报错。 如果函数没有标注 open
如
Base.nv()
,则子类中不允许定义相同签名的函数, 不论加不加 override
。在一个 final
类中(没有用 open
标注的类),开放成员是禁止的。
标记为
override
的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,使用
final
关键字:
open class AnotherDerived (
)
: Base (
)
{
final override fun v (
)
{
}
}
覆盖属性
属性覆盖与方法覆盖类似;在超类中声明然后在派生类中重新声明的属性必须以
override
开头,并且它们必须具有兼容的类型。每个声明的属性可以由具有初始化器的属性或者具有 getter 方法的属性覆盖。
open class Foo {
open val x : Int get
(
)
{
……
}
}
class
Bar1
:
Foo
(
)
{
override val
x
:
Int
=
……
}
你也可以用一个
var
属性覆盖一个
val
属性,但反之则不行。这是允许的,因为一个
val
属性本质上声明了一个 getter 方法,而将其覆盖为
var
只是在子类中额外声明一个 setter 方法。
请注意,你可以在主构造函数中使用
override
关键字作为属性声明的一部分。
interface Foo {
val count : Int
}
class Bar1 ( override val count : Int )
: Foo
class Bar2 : Foo {
override var count : Int= 0
}
调用超类实现
派生类中的代码可以使用
super
关键字调用其超类的函数与属性访问器的实现:
open class Foo {
open fun f (
)
{
println
(
“Foo.f()”
)
}
open val
x
:
Int get
(
)
=
1
}
class Bar : Foo (
)
{
override fun f (
)
{
super
. f (
)
println (
“Bar.f()”
)
}
override val x : Int get
(
)
super
. x+ 1
}
在一个内部类中访问外部类的超类,可以通过由外部类名限定的
super
关键字来实现:
super@Outer
class Bar : Foo (
)
{
override fun f (
)
{
/* …… */
}
override val x : Int get
(
)
0
inner class Baz {
fun g (
)
{
super @Bar . f (
)
// 调用 Foo 实现的 f()
println ( super @Bar . x )
// 使用 Foo 实现的 x 的 getter
}
}
}
覆盖规则
在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。 为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的
super
,如
super<Base>
:
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()
}
}
同时继承
A
和
B
没问题,并且
a()
和
b()
也没问题因为
C
只继承了每个函数的一个实现。 但是
f()
由
C
继承了两个实现,所以我们 必须
在
C
中覆盖
f()
并且提供我们自己的实现来消除歧义。
抽象类
类和其中的某些成员可以声明为
abstract
。 抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用
open
标注一个抽象类或者函数——因为这不言而喻
我们可以用一个抽象成员覆盖一个非抽象的开放成员
open class Base {
open fun f() {}
}
abstract class Derived : Base() {
override abstract fun f()
}
属性和字段
声明属性
Kotlin的类可以有属性。 属性可以用关键字
var
声明为可变的,否则使用只读关键字
val
。
class Address {
var name : String=……
var street : String=……
var city : String=……
var state : String?=……
var zip : String=……
}
要使用一个属性,只要用名称引用它即可,就像 Java 中的字段:
fun copyAddress ( address : Address )
: Address {
val result=Address (
)
// Kotlin 中没有“new”关键字
result . name=address . name // 将调用访问器
result . street=address . street
// ……
return result
}
声明一个属性的完整语法是
var
:
[ =<property_initializer> ]
[
[
其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器 (或者从其 getter 返回值,如下文所示)中推断出来,也可以省略。
var allByDefault : Int? // 错误:需要显式初始化器,隐含默认 getter 和 setter
var initialized= 1
// 类型 Int、默认getter 和 setter
我们可以编写自定义的访问器,非常像普通函数,刚好在属性声明内部。这里有一个自定义 getter 的例子:
val isEmpty : Boolean
get
(
)
this
. size== 0
自定义的 setter:
var stringRepresentation : String
get
(
)
this
. toString (
)
set
( value )
{
setDataFromString ( value )
// 解析字符串并赋值给其他属性
}
按照惯例,setter 参数的名称是
value
,但是如果你喜欢你可以选择一个不同的名称
自 Kotlin 1.1 起,如果可以从 getter 推断出属性类型,则可以省略它:
val isEmpty get
(
)
this
. size== 0
// 具有类型 Boolean
如果你需要改变一个访问器的可见性或者对其注解,但是不需要改变默认的实现, 你可以定义访问器而不定义其实现:
v ar setterVisibility : String= “abc”
private set
// 此 setter 是私有的并且有默认实现
var setterWithAnnotation : Any?= null
@Inject set
// 用 Inject 注解此 setter
幕后字段
Kotlin 中类不能有字段。然而,当使用自定义访问器时,有时有一个幕后字段(backing field)有时是必要的。为此 Kotlin 提供
一个自动幕后字段,它可通过使用
field
标识符访问。
var counter= 0
// 此初始器值直接写入到幕后字段
set
( value )
{
if
( value>= 0
)
field =value
}
field
标识符只能用在属性的访问器内
如果属性至少一个访问器使用默认实现,或者自定义访问器通过
field
引用幕后字段,将会为该属性生成一个幕后字段。
例如,下面的情况下, 就没有幕后字段:
val isEmpty : Boolean
get
(
)
this
. size== 0
幕后属性
如果你的需求不符合这套“隐式的幕后字段”方案,那么总可以使用
幕后属性(backing property)
:
private var _table : Map<String , Int>?= null
public val table : Map<String , Int>
get
(
)
{
if
( _table== null
)
{
_table=HashMap (
)
// 类型参数已推断出
}
return _table? : throw AssertionError (
“Set to null by another thread”
)
}
从各方面看,这正是与 Java 相同的方式。因为通过默认 getter 和 setter 访问私有属性会被优化,所以不会引入函数调用开销。
编译期常量
已知值的属性可以使用
const
修饰符标记为
编译期常量
。 这些属性需要满足以下要求:
- 位于顶层或者是
object
的一个成员,用String
或原生类型 值初始化,没有自定义 getter。
这些属性可以用在注解中:
const val SUBSYSTEM_DEPRECATED : String= “This subsystem is deprecated”
@Deprecated ( SUBSYSTEM_DEPRECATED ) fun foo (
)
{ …… }
延迟初始化属性
一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检查。
为处理这种情况,你可以用
lateinit
修饰符标记该属性:
public class MyTest {
lateinit var subject : TestSubject
@SetUp fun setup (
)
{
subject=TestSubject (
)
}
@Test fun test (
)
{
subject . method (
)
// 直接解引用
}
}
该修饰符只能用于在类体中(不是在主构造函数中)声明的
var
属性,并且仅
当该属性没有自定义 getter 或 setter 时。该属性必须是非空类型,并且不能是
原生类型。
在初始化前访问一个
lateinit
属性会抛出一个特定异常,该异常明确标识该属性
被访问及它没有初始化的事实。
该修饰符只能用于在类体中(不是在主构造函数中)声明的
var
属性,并且仅
当该属性没有自定义 getter 或 setter 时。该属性必须是非空类型,并且不能是
原生类型。
在初始化前访问一个
lateinit
属性会抛出一个特定异常,该异常明确标识该属性
被访问及它没有初始化的事实。
接口
Kotlin 的接口与 Java 8 类似,既包含抽象方法的声明,也包含
实现。与抽象类不同的是,接口无法保存状态。它可以有
属性但必须声明为抽象或提供访问器实现, 在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器
不能引用它们 。
interface MyInterface {
fun bar (
)// 默认的访问器
fun foo (
)
{
// 可选的方法体
}
}
interface MyInterface {
val prop : Int // 抽象的
val propertyWithImplementation : String
get
(
)
“foo”
fun foo (
)
{
print ( prop )
}
}
class Child : MyInterface {
override val prop : Int= 29
}
包
函数、属性和类、对象和接口可以在顶层声明,即直接在包内:
// 文件名:example.kt
package foo
fun baz (
)
{
}
class Bar {
}
- 如果你不指定任何可见性修饰符,默认为
public
,这意味着你的声明将随处可见,如果你声明为private
,它只会在声明它的文件内可见,如果你声明为internal
,它会在相同 内随处可见,protected
不适用于顶层声明。
类和接口
对于类内部声明的成员:
private
意味着只在这个类内部(包含其所有成员)可见;protected
—— 和private
一样 + 在子类中可见。internal
—— 能见到类声明的 本模块内 的任何客户端都可见其internal
成员;public
—— 能见到类声明的任何客户端都可见其public
成员。
注意 对于Java用户:Kotlin 中外部类不能访问内部类的 private 成员。
如果你覆盖一个
protected
成员并且没有显式指定其可见性,该成员还会是
protected
可见性。
扩展
Kotlin 同 C# 和 Gosu 类似,能够扩展一个类的新功能而无需继承该类或使用像装饰者这样的任何类型的设计模式。 这通过叫做
扩展
的特殊声明完成。Kotlin 支持
扩展函数
和
扩展属性
。
扩展函数
声明一个扩展函数,我们需要用一个
接收者类型
也就是被扩展的类型来作为他的前缀。 下面代码为
MutableList<Int>
添加一个
swap
函数:
fun
MutableList
{
val tmp= this
[ index1 ]
// “this”对应该列表
this
[ index1 ]
this
[ index2 ]
this
[ index2 ] =tmp
}
这个
this
关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象) 现在,我们对任意
MutableList<Int>
调用该函数了:
val l=mutableListOf (
1
,
2
,
3
)
l . swap (
0
,
2
)
// “swap()”内部的“this”得到“l”的值
当然,这个函数对任何
MutableList<T>
起作用,我们可以泛化它:
fun
{
val tmp= this
[ index1 ]
// “this”对应该列表
this
[ index1 ]
this
[ index2 ]
this
[ index2 ] =tmp
}
扩展是静态解析的
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数。
我们想强调的是扩展函数是静态分发的,即他们不是根据接收者类型的虚方法。 这意味着调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。例如:
open class C
class D : C (
)
fun C . foo (
)
“c”
fun D . foo (
)
“d”
fun printFoo ( c : C )
{
println ( c . foo (
)
)
} printFoo ( D (
)
)
这个例子会输出 “c”,因为调用的扩展函数只取决于
参数
c
的声明类型,该类型是
C
类。
如果一个类定义有一个成员函数和一个扩展函数,而这两个函数又有相同的接收者类型、相同的名字
并且都适用给定的参数,这种情况 总是取成员函数
。 例如:
class C {
fun foo (
)
{ println (
“member”
)
}
}
fun C . foo (
)
{ println (
“extension”
)
}
如果我们调用
C
类型
c
的
c.foo()
,它将输出“member”,而不是“extension”。
当然,扩展函数重载同样名字但不同签名成员函数也完全可以:
class C {
fun foo (
)
{ println (
“member”
)
}
}
fun C . foo ( i : Int )
{ println (
“extension”
)
}
调用
C().foo(1)
将输出 “extension”。
可空接收者
注意可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为 null,并且可以在函数体内检测
this == null
,这能让你
在没有检测 null 的时候调用 Kotlin 中的toString():检测发生在扩展函数的内部。
fun Any? . toString (
)
: String {
if
( this
null
) return
“null”
// 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
// 解析为 Any 类的成员函数
return toString (
)
}
扩展属性
和函数类似,Kotlin 支持扩展属性:
val
get
(
) =size- 1
注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说
是无效的。这就是为什么 扩展属性不能有初始化器
。他们的行为只能由显式提供的 getters/setters 定义。
val Foo . bar= 1
// 错误:扩展属性不能有初始化器
var Outer.index : Int
get() = 1
set(value) {
index = value
}
伴生对象的扩展
如果一个类定义有一个
,你也可以为伴生对象定义
扩展函数和属性:
class MyClass {
companion object
{
}
// 将被称为 “Companion”
}
fun MyClass . Companion . foo (
)
{
// ……
}
就像伴生对象的其他普通成员,只需用类名作为限定符去调用他们
MyClass . foo (
)
扩展声明为成员
在一个类内部你可以为另一个类声明扩展。在这样的扩展内部,有多个
隐式接收者
—— 其中的对象成员可以无需通过限定符访问。扩展声明所在的类的实例称为
分发接收者
,扩展方法调用所在的接收者类型的实例称为
扩展接收者
。
class D {
fun bar (
)
{ …… }
}
class C {
fun baz (
)
{ …… }
fun D . foo (
)
{
bar (
)
// 调用 D.bar
baz (
)
// 调用 C.baz
}
fun
caller
(
d
:
D
)
{
d
.
foo
(
)
// 调用扩展函数
}
}
对于分发接收者和扩展接收者的成员名字冲突的情况,扩展接收者
优先。要引用分发接收者的成员你可以使用
。
class C {
fun D . foo (
)
{
toString (
)
// 调用 D.toString()
this @C . toString (
)
// 调用 C.toString()
}
声明为成员的扩展可以声明为
open
并在子类中覆盖。这意味着这些函数的分发
对于分发接收者类型是虚拟的,但对于扩展接收者类型是静态的。
open class D {
}
class D1 : D (
)
{
}
open class C {
open fun D . foo (
)
{
println (
“D.foo in C”
)
}
open fun D1 . foo (
)
{
println (
“D1.foo in C”
)
}
fun caller ( d : D )
{
d . foo (
)
// 调用扩展函数
}
}
class C1 : C (
)
{
override fun D . foo (
)
{
println (
“D.foo in C1”
)
}
override fun D1 . foo (
)
{
println (
“D1.foo in C1”
)
}
}
C (
)
. caller ( D (
)
)
// 输出 “D.foo in C”
C1 (
)
. caller ( D (
)
)
// 输出 “D.foo in C1”
—— 分发接收者虚拟解析
C
(
)
.
caller
(
D1
(
)
)
// 输出 “D.foo in C” —— 扩展接收者静态解析
数据类
我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从
数据机械推导而来的。在 Kotlin 中,这叫做
数据类
并标记为
data
:
data class User ( val name : String , val age : Int )
编译器自动从主构造函数中声明的所有属性导出以下成员:
equals()
/
hashCode()
对
toString()
格式是
"User(name=John, age=42)"
;
按声明顺序对应于所有属性;
copy()
函数(见下文)
为了确保生成的代码的一致性和有意义的行为,数据类必须满足以下要求:
主构造函数需要至少有一个参数
主构造函数的所有参数需要标记为
val
或
var
数据类不能是抽象、开放、密封或者内部的
(在1.1之前)数据类只能实现接口
此外,成员生成遵循关于成员继承的这些规则:
如果在数据类体中有显式实现
equals()
、
hashCode()
或者
toString()
,或者这些函数在父类中有
final
实现,那么不会生成这些函数,而会使用现有
函数;
如果超类型具有
open
的
componentN()
函数并且返回兼容的类型, 那么会为数据类生成相应的函数,并覆盖超类的实现。如果超类型的这些函数
由于签名不兼容或者是 final 而导致无法覆盖,那么会报错
不允许为
componentN()
以及
copy()
函数提供显式实现
在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值。
data class User ( val name : String= ""
, val age : Int= 0
)
复制
在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。
copy()
函数就是为此而生成。对于上文的
User
类,其实现会类似下面这样:
fun copy ( name : String= this
. name , age : Int= this
. age ) =User ( name , age )
这让我们可以写:
val jack=User ( name= “Jack”
, age= 1
)
val olderJack=jack . copy ( age= 2
)
数据类和解构声明
为数据类生成的
Component 函数
使它们可在
中使用:
val jane=User (
“Jane”
,
35
)
val
( name , age ) =jane
println (
“$name, $age years of age”
)
// 输出 “Jane, 35 years of age”
标准数据类
标准库提供了
Pair
和
Triple
。尽管在很多情况下命名数据类是更好的设计选择, 因为它们通过为属性提供有意义的名称使代码更具可读性。
密封类
密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
要声明一个密封类,需要在类名前面添加
sealed
修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。(在 Kotlin 1.1 之前, 该规则更加严格:子类必须嵌套在密封类声明的内部)。
sealed class Expr
data class Const ( val number : Double )
: Expr (
)
data class Sum ( val e1 : Expr , val e2 : Expr )
: Expr (
)
object NotANumber : Expr (
)
(上文示例使用了 Kotlin 1.1 的一个额外的新功能:数据类扩展包括密封类在内的其他类的可能性。 )
一个密封类是自身
,它不能直接实例化并可以有抽象(
abstract
)成员。
密封类不允许有非-
private
构造函数(其构造函数默认为
private
)。
请注意,扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在
同一个文件中。
使用密封类的关键好处在于使用
的时候,如果能够
验证语句覆盖了所有情况,就不需要为该语句再添加一个
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
子句,因为我们已经覆盖了所有的情况
}
泛型
与 Java 类似,Kotlin 中的类也可以有类型参数, 一般来说,要创建这样类的实例,我们需要提供类型参数 :
class
Box
{
var value=t
}
val
box
:
Box
1
)
但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数:
val box=Box (
1
)
// 1 具有类型 Int,所以编译器知道我们说的是 Box
声明处型变
假设有一个泛型接口
Source<T>
,该接口中不存在任何以
T
作为参数的方法,只是方法返回
T
类型值:
// Java
interface
Source
TnextT (
)
;
}
那么,在
Source <Object>
类型的变量中存储
Source <String>
实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:
// Java
voiddemo
(
Source
{
Source
// !!!在 Java 中不允许
// ……
}
为了修正这一点,我们必须声明对象的类型为
Source<? extends Object>
,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。
在 Kotlin 中,有一种方法向编译器解释这种情况。这称为
声明处型变
:我们可以标注
Source
的
类型参数
T
来确保它仅从
Source<T>
成员中
返回
(生产),并从不被消费。 为此,我们提供
out
修饰符:
abstract class Source< out T> {
abstract fun nextT (
)
: T
}
fun
demo
(
strs
:
Source
{
val
objects
:
Source
// ……
}
一般原则是:当一个类
C
的类型参数
T
被声明为 out
时,它就只能出现在
C
的成员的 输出
-位置,但回报是
C<Base>
可以安全地作为
C<Derived>
的超类。
简而言之,他们说类
C
是在参数
T
上是 协变的
,或者说
T
是一个 协变的
类型参数。 你可以认为
C
是
T
的 生产者
,而不是
T
的 消费者
。
out
修饰符称为 型变注解
,并且由于它在类型参数声明处提供,所以我们讲 声明处型变
。 这与 Java 的 使用处型变
相反,其类型用途通配符使得类型协变。
另外除了 out
,Kotlin 又补充了一个型变注释: in
。它使得一个类型参数 逆变
:只可以被消费而不可以
被生产。逆变类的一个很好的例子是
Comparable
:
abstract class Comparable< in T> {
abstract fun compareTo ( other : T )
: Int
}
fun
demo
(
x
:
Comparable
{
x . compareTo (
1.0
)
// 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable
val
y
:
Comparable
}
我们相信 in
和 out
两词是自解释的(因为它们已经在 C# 中成功使用很长时间了), 因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:
类型投影
使用处型变:类型投影
将类型参数 T 声明为
out
非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上 不能
限制为只返回
T
! 一个很好的例子是 Array:
class
Array
size :
Int )
{
fun
get
( index :
Int )
:
T
{
///* …… */ }
fun
set
( index :
Int ,
value :
T )
{
///* …… */ }
}
该类在
T
上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数:
fun
copy ( from :
Array
to :
Array
{
assert ( from . size
==
to . size )
for
( i
in
from . indices )
to [ i ]
=
from [ i ]
}
这个函数应该将项目从一个数组复制到另一个数组。让我们尝试在实践中应用它:
val
ints :
Array
=
arrayOf (
1
,
2
,
3
)
val
any
=
Array
3
)
{
""
}
copy ( ints ,
any )
// 错误:期望 (Array
这里我们遇到同样熟悉的问题:
Array <T>
在
T
上是 不型变的
,因此
Array <Int>
和
Array <Any>
都不是
另一个的子类型。为什么? 再次重复,因为 copy 可能
做坏事,也就是说,例如它可能尝试 写
一个 String 到
from
, 并且如果我们实际上传递一个
Int
的数组,一段时间后将会抛出一个
ClassCastException
异常。
那么,我们唯一要确保的是
copy()
不会做任何坏事。我们想阻止它 写
到
from
,我们可以:
fun
copy ( from :
Array< out
Any> ,
to :
Array
{
// ……
}
这里发生的事情称为
类型投影
:我们说
from
不仅仅是一个数组,而是一个受限制的(
投影的
)数组:我们只可以调用返回类型为类型参数
T
的方法,如上,这意味着我们只能调用
get()
。这就是我们的
使用处型变
的用法,并且是对应于 Java 的
Array<? extends Object>
、 但使用更简单些的方式。
你也可以使用 in 投影一个类型:
fun
fill ( dest :
Array< in
String> ,
value :
String )
{
// ……
}
Array<in String>
对应于 Java 的
Array<? super String>
,也就是说,你可以传递一个
CharSequence
数组或一个
Object
数组给
fill()
函数。
星投影
有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。
Kotlin 为此提供了所谓的 星投影 语法:
- 对于
Foo <out T>
,其中T
是一个具有上界TUpper
的协变类型参数,Foo <*>
等价于Foo <out TUpper>
。 这意味着当T
未知时,你可以安全地从Foo <*>
读取TUpper
的值。 - 对于
Foo <in T>
,其中T
是一个逆变类型参数,Foo <*>
等价于Foo <in Nothing>
。 这意味着当T
未知时,没有什么可以以安全的方式 写入Foo <*>
。 - 对于
Foo <T>
,其中T
是一个具有上界TUpper
的不型变类型参数,Foo<*>
对于读取值时等价于Foo<out TUpper>
而对于写值时等价于Foo<in Nothing>
。
如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为
interface Function <in T, out U>
,我们可以想象以下星投影:
Function<*, String>
表示Function<in Nothing, String>
;Function<Int, *>
表示Function<Int, out Any?>
;Function<*, *>
表示Function<in Nothing, out Any?>
。
注意 :星投影非常像 Java 的原始类型,但是安全。
泛型函数
不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:
fun
singletonList ( item :
T )
:
List
{
// ……
}
fun
T . basicToString (
)
:
String
{
// 扩展函数
// ……
}
要调用泛型函数,在调用处函数名 之后 指定类型参数即可:
val l = singletonList<Int>(1)
泛型约束
能够替换给定类型参数的所有可能类型的集合可以由 泛型约束
限制。
上界
最常见的约束类型是与 Java 的 extends 关键字对应的 上界 :
fun <T : Comparable<T>> sort(list: List<T>) {
// ……
}
冒号之后指定的类型是 上界
:只有
Comparable<T>
的子类型可以替代
T
。 例如:
sort ( listOf (
1
,
2
,
3
)
)
// OK。Int 是 Comparable
sort ( listOf ( HashMap<Int ,
String> (
)
)
)
// 错误:HashMap<Int, String> 不是
Comparable<HashMap<Int, String» 的子类型
默认的上界(如果没有声明)是
Any?
。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,我们需要一个单独的 where
-子句:
fun
cloneWhenGreater ( list :
List
threshold :
T )
:
List
where
T
:
Comparable
,
T
:
Cloneable
{
return
list . filter
{
it
threshold
}
. map
{
it . clone (
)
}
}
嵌套类与内部类
类可以嵌套在其他类中:
class
Outer
{
private
val
bar :
Int
=
1
class
Nested
{
fun
foo (
)
=
2
}
}
val
demo
=
Outer . Nested (
)
. foo (
)
// == 2
内部类
类可以标记为
inner
以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用:
class
Outer
{
private
val
bar :
Int
=
1
inner
class
Inner
{
fun
foo (
)
=
bar
}
}
val
demo
=
Outer (
)
. Inner (
)
. foo (
)
// == 1
匿名内部类
使用 创建匿名内部类实例:
window.addMouseListener(object: MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ……
}
override fun mouseEntered(e: MouseEvent) {
// ……
}
})
如果对象是函数式 Java 接口(即具有单个抽象方法的 Java 接口)的实例, 你可以使用带接口类型前缀的lambda表达式创建它:
val listener = ActionListener { println("clicked") }
枚举类
枚举类的最基本的用法是实现类型安全的枚举:
enum
class
Direction
{
NORTH ,
SOUTH ,
WEST ,
EAST
}
枚举类
枚举类的最基本的用法是实现类型安全的枚举:
enum
class
Direction
{
NORTH ,
SOUTH ,
WEST ,
EAST
}
每个枚举常量都是一个对象。枚举常量用逗号分隔。
初始化
因为每一个枚举都是枚举类的实例,所以他们可以是这样初始化过的:
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
}
使用枚举常量
就像在 Java 中一样,Kotlin 中的枚举类也有合成方法允许列出
定义的枚举常量以及通过名称获取枚举常量。这些方法的
签名如下(假设枚举类的名称是
EnumClass
):
EnumClass . valueOf ( value :
String )
:
EnumClassEnumClass . values (
)
:
Array
如果指定的名称与类中定义的任何枚举常量均不匹配,
valueOf()
方法将抛出
IllegalArgumentException
异常。
自 Kotlin 1.1 起,可以使用
enumValues<T>()
和
enumValueOf<T>()
函数以泛型的方式访问枚举类中的常量 :
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}
printAllValues<RGB>() // 输出 RED, GREEN, BLUE
每个枚举常量都具有在枚举类声明中获取其名称和位置的属性:
val name: String
val ordinal: Int
对象表达式和对象声明
有时候,我们需要创建一个对某个类做了轻微改动的类的对象,而不用为之显式声明新的子类。 Java 用
匿名内部类
处理这种情况。 Kotlin 用
对象表达式
和
对象声明
对这个概念稍微概括了下。
对象表达式
要创建一个继承自某个(或某些)类型的匿名类的对象,我们会这么写:
window . addMouseListener ( object
:
MouseAdapter (
)
{
override
fun
mouseClicked ( e :
MouseEvent )
{
// ……
}
override
fun
mouseEntered ( e :
MouseEvent )
{
// ……
}
}
)
如果超类型有一个构造函数,则必须传递适当的构造函数参数给它。 多个超类型可以由跟在冒号后面的逗号分隔的列表指定:
open class A(x: Int) {
public open val y: Int = x
}
interface B {……}
val ab: A = object : A(1), B {
override val y = 15
}
任何时候,如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写:
fun
foo (
)
{
val
adHoc
=
object
{
var
x :
Int
=
0
var
y :
Int
=
0
}
print ( adHoc . x
adHoc . y )
}
请注意,匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是
Any
。在匿名对象中添加的成员将无法访问。
class C {
// 私有函数,所以其返回类型是匿名对象类型
private fun foo() = object {
val x: String = "x"
}
// 公有函数,所以其返回类型是 Any
fun publicFoo() = object {
val x: String = "x"
}
fun bar() {
val x1 = foo().x // 没问题
val x2 = publicFoo().x // 错误:未能解析的引用“x”
}
}
就像 Java 匿名内部类一样,对象表达式中的代码可以访问来自包含它的作用域的变量。 (与 Java 不同的是,这不仅限于 final 变量。)
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
override fun mouseEntered(e: MouseEvent) {
enterCount++
}
})
// ……
}