类与对象
更新: 6/27/2025 字数: 0 字 时长: 0 分钟
封装的意义是什么?这是很多初学者都会有的疑惑。
我们可以简单的认为封装的目的是安全,通过封装我们在一定程度上让对象的类封闭,使用者在调用对象方法与字段时尽可以通过我们为其设定好的方法来获取,使得一切都在我们的掌握之中。
进一步讲,封装也可以帮我们实现充血模型,通过对get/set方法的修改与重写,我们可以让一个类的字段的设置封死在我们的类中,更加符合开闭原则
继承了Java的特点Kotlin自然也继承了Java面向对象的特点,同时又发展出了自己的特色,这里我们简单来聊一下Kotlin的类与对象
class Person(
val name: String,
val age: Int
) {
var children: MutableList<Person> =mutableListOf()
constructor(name: String,children: MutableList<Person>,child:Person) : this(name,0) {
Person(name,0)
this.children = children
children.add(child)
}
}
上面就是一个Kotlin方法的写法
构造函数
Kotlin区分为主构造函数和次构造函数,其中主构造函数就是类中的
class Person(
val name: String,
val age: Int =0,
)
他表示了创建一个类至少需要哪些参数,我们也可以通过赋默认值的方式让这个参数变为不必须的。
其他的参数(比如上面的Children参数)就是一个不在构造函数中的参数,由于他不在受主构造函数的限制,因此要求我们必须为他赋初值来规避空的问题
除了主构造函数外的函数我们称之为副构造函数
constructor(name: String,children: MutableList<Person>,child:Person) : this(name,0) {
Person(name,0)
this.children = children
children.add(child)
}
副构造函数可以有多个,但返回值必须是主构造创建的类(官方说法叫做委托给主构造函数),同时你可以在副构造函数的函数体中完成你想要的一些操作
继承
Kotlin中的类默认是final的,也就是不可以被继承,你可以通过显示的添加open字段来使其打开
open class Father{
}
class Son:Father(){
}
需要注意的是,为了空安全,我们必须在继承的时候通过主构造函数传入父类必要的参数
get/set方法
对于主构造函数中的方法无法直接创建get/set方法,你可以间接的通过这种方式创建
class Son(
name: String
){
var name: String = name
get()=field
set(value){
field=value
}
}
特别值得注意的是,在get/set方法中你必须使用特定的field参数来表示当前字段,这个参数是为了避免无限回调的
当你写成
class Son(
name: String
){
var name: String = name
get()=name
set(value){
field=value
}
}
这时你调用Son的name方法会直接StackOverFlow,这是因为get方法内部调用name时又会使用name的get方法,接着就会无限下去
函数为一等公民
Java8中最受欢迎的新特性应该就是stream了,他间接性的让Java拥有了一定的函数是编程的特性,Kotlin为了彻底将这一特性融入自己的底层,于是将函数作为自己的一等公民,通过这一特性,使得Kotlin彻底成为一个支持函数式编程的语言
头等函数(first-class function;第一级函数)是指在程序设计语言中,函数被当作头等公民。这意味着,函数可以作为别的函数的参数、函数的返回值,赋值给变量或存储在数据结构中。有人主张应包括支持匿名函数(函数字面量,function literals)。在这样的语言中,函数的名字没有特殊含义,它们被当作具有函数类型的普通的变量对待。1960年代中期,克里斯托弗·斯特雷奇在“functions as first-class citizens”中提出这一概念。——维基百科
现在你可以这样写代码
fun outfun(innerfun: (i: Int) -> Boolean): Boolean
这个函数传入一个入参为int并使用boolean的命名innerfun的函数,在outfun这个函数中,你可以任意的使用innerfun这个函数
亦或者你也可以直接在一个kt文件中写一堆的函数而没有类,这都是被允许的(现在类不再被强制要求写在方法中了,好耶!!!)
泛型
引入in,out,删除 ?
通配符
在Java中,如果有这么一个接口以及对接口的调用
interface Out<T> {
T nextT();
}
public static void demo(Out<String> strs) {
Out<Object> objects = strs;
Object obj = objects.nextT();
System.out.println(obj);
}
这是不能通过编译的,因为Java中的泛型是不形变的(也就是说Out< String >和Out< Object > 之间不存在某种继承关系)
这样是能保证安全的,但是不大方便。因为我们确实知道Object是String的父类,那这里返回String应该用Object接受也好
所以Java引入了?通配符,于是你就可以把代码写成这样
public static void demo(Out<String> strs) {
Out<? extends Object> objects = strs;
Object obj = objects.nextT(); //不再报错
System.out.println(obj);
}
那么为什么Java认为第一种写法不安全呢?
首先,泛型接口是一个黑盒操作,也就是说写泛型接口实现类的人完全不知道你传进来了个什么东西,也完全不知道自己要返回个什么东西,他只能自己给泛型固定一种类(这里不讨论继续使用泛型的情况,因为那就相当与使用Object或者将问题继续下抛)
我们先假定这时写实现类的人将泛型定义成了String类。
如果作者写一个返回值是String的函数,那么返回值就一定是一个调用String api安全的类,也就是说,作者考虑到了你使用String的情况,也就是说你只要使用String的父类就一定是安全的。
同样的,如果作者写的是一个传入值是String的函数,那么他就默认你在使用该类的时候传入的一定是一个调用String api安全的类,也就是说你传入的只要是String的子类对作者来说就是安全的。
不过,如果一个函数的传入值和传出值都有String(也就是我们的泛型),这就同时要求你传入的是String自己及其子类,而获取的必须是String自己及其父类。这看似好像是没问题,但请不要忘记,你在调用这个接口的时候,你也是要指定泛型的类型的(因为这决定了你后面传入和接受的时候究竟要传入和接受一个什么样的类),而当你指定了这个类型后,就代表你在使用时也要满足是String自己及其父类且是一个String自己及其子类的情况,这也就固死了这个类只能是String。
因此也就分为了三种情况
请况 | 要求 |
---|---|
当泛型仅作为入参时 | 必须使用其本身与子类 |
当泛型仅作为出参时 | 必须使用其本身与父类 |
当泛型同时为出参与入参时 | 必须使用泛型本身 |
Joshua Bloch 在其著作《Effective Java》第三版 中很好地解释了该问题 (第 31 条:“利用有限制通配符来提升 API 的灵活性”)。 他称那些你只能从中读取的对象为生产者, 并称那些只能向其写入的对象为消费者。
解释为我们上面的分类也就是将其修改为了这样
请况 | 要求 | 类型 | Java中的情况 |
---|---|---|---|
当泛型仅作为入参时 | 必须使用其本身与子类 | 消费者 | ? super T |
当泛型仅作为出参时 | 必须使用其本身与父类 | 生产者 | ? extends T |
当泛型同时为出参与入参时 | 必须使用泛型本身 | --- | --- |
在Kotlin中,为了规避这种复杂的分类,同时也是为了删除不直观的?xx T的形式我们引入in out关键字
其中特别要求,如果在定义泛型接口的时候,使用了in关键字修饰泛型T,那么其方法中泛型T只能作为入参,不能出现在出参中,而使用了out关键字修饰泛型T,那么其方法中泛型T只能作为出参,不能出现在如参中
现在也就变成了这样
请况 | 要求 | 类型 | Java中的情况 | Kotlin的关键字 | Kotlin中的要求 |
---|---|---|---|---|---|
当泛型仅作为入参时 | 必须使用其本身与子类 | 消费者 | ? super T | in | 泛型只能出现在入参中 |
当泛型仅作为出参时 | 必须使用其本身与父类 | 生产者 | ? extends T | out | 泛型只能出现在出参中 |
当泛型同时为出参与入参时 | 必须使用泛型本身 | --- | --- | --- | --- |
interface In<in T> {
fun test(t: T)
}
interface Out<out T> {
fun test(): T
}
fun test(inT: In<Number>, outT: Out<String>) {
val outterVal: Any = outT.test()
val innerVal: Int = 1
inT.test(t1)
}
类型投影 *
Kotlin中还存在一种名为类型投影的语法,主要用于is as中表示不在乎泛型类的泛型是什么,只在乎是不是这个泛型类
val a=mutableListOf<Int>()
println(a is MutableList<*>) //true 这里的*表示任意类
原生支持委托模式
委托模式(delegation pattern) 是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承,它还使我们可以模拟mixin。——维基百科
在Kotlin中,你可以使用by关键字实现委托模式,这需要你在主构造函数中传入一个被委托者,然后再后面使用by方法完成委托,委托者拥有被委托者的所有方法,但不会继承参数
interface Base {
val message: String
fun print()
}
class BaseImpl(x: Int) : Base {
override val message = "BaseImpl: x = $x"
override fun print() { println(message) }
}
class Derived(b: Base) : Base by b {
// 在 b 的 `print` 实现中不会访问到这个属性
override val message = "Message of Derived"
}
fun main() {
val b = BaseImpl(10)
val derived = Derived(b)
derived.print()
println(derived.message)
println(derived.x)//爆红
}