主构造器
我们之前已经了解了 Kotlin 中 constructor 的写法:
🏝️
class User {
var name: String
constructor(name: String) {
this.name = name
}
}
其实 Kotlin 中还有更简单的方法来写构造器:
🏝️
👇
class User constructor(name: String) {
// 👇 这里与构造器中的 name 是同一个
var name: String = name
}
这里有几处不同点:
constructor
构造器移到了类名之后- 类的属性
name
可以引用构造器中的参数name
这个写法叫「主构造器 primary constructor」。与之相对的在第二篇中,写在类中的构造器被称为「次构造器」。在 Kotlin 中一个类最多只能有 1 个主构造器(也可以没有),而次构造器是没有个数限制。
主构造器中的参数除了可以在类的属性中使用,还可以在 init
代码块中使用:
🏝️
class User constructor(name: String) {
var name: String
init {
this.name = name
}
}
其中 init
代码块是紧跟在主构造器之后执行的,这是因为主构造器本身没有代码体,init
代码块就充当了主构造器代码体的功能。
另外,如果类中有主构造器,那么其他的次构造器都需要通过 this
关键字调用主构造器,可以直接调用或者通过别的次构造器间接调用。如果不调用 IDE 就会报错:
🏝️
class User constructor(var name: String) {
constructor(name: String, id: Int) {
// 👆这样写会报错,Primary constructor call expected
}
}
为什么当类中有主构造器的时候就强制要求次构造器调用主构造器呢?
我们从主构造器的特性出发,一旦在类中声明了主构造器,就包含两点:
- 必须性:创建类的对象时,不管使用哪个构造器,都需要主构造器的参与
- 第一性:在类的初始化过程中,首先执行的就是主构造器
这也就是主构造器的命名由来。
当一个类中同时有主构造器与次构造器的时候,需要这样写:
🏝️
class User constructor(var name: String) {
// 👇 👇 直接调用主构造器
constructor(name: String, id: Int) : this(name) {
}
// 👇 通过上一个次构造器,间接调用主构造器
constructor(name: String, id: Int, age: Int) : this(name, id) {
}
}
在使用次构造器创建对象时,init
代码块是先于次构造器执行的。如果把主构造器看成身体的头部,那么 init
代码块就是颈部,次构造器就相当于身体其余部分。
细心的你也许会发现这里又出现了 :
符号,它还在其他场合出现过,例如:
- 变量的声明:
var id: Int
- 类的继承:
class MainActivity : AppCompatActivity() {}
- 接口的实现:
class User : Impl {}
- 匿名类的创建:
object: ViewPager.SimpleOnPageChangeListener() {}
- 函数的返回值:
fun sum(a: Int, b: Int): Int
可以看出 :
符号在 Kotlin 中非常高频出现,它其实表示了一种依赖关系,在这里表示依赖于主构造器。
通常情况下,主构造器中的 constructor
关键字可以省略:
🏝️
class User(name: String) {
var name: String = name
}
但有些场景,constructor
是不可以省略的,例如在主构造器上使用「可见性修饰符」或者「注解」:
- 可见性修饰符我们之前已经讲过,它修饰普通函数与修饰构造器的用法是一样的,这里不再详述:
🏝️
class User private constructor(name: String) {
// 👆 主构造器被修饰为私有的,外部就无法调用该构造器
}
- 关于注解的知识点,我们之后会讲,这里就不展开了
既然主构造器可以简化类的初始化过程,那我们就帮人帮到底,送佛送到西,用主构造器把属性的初始化也一并给简化了。
主构造器里声明属性
之前我们讲了主构造器中的参数可以在属性中进行赋值,其实还可以在主构造器中直接声明属性:
🏝️
👇
class User(var name: String) {
}
// 等价于:
class User(name: String) {
var name: String = name
}
如果在主构造器的参数声明时加上 var
或者 val
,就等价于在类中创建了该名称的属性(property),并且初始值就是主构造器中该参数的值。
以上讲了所有关于主构造器相关的知识,让我们总结一下类的初始化写法:
- 首先创建一个
User
类:
🏝️
class User {
}
- 添加一个参数为
name
与id
的主构造器:
🏝️
class User(name: String, id: String) {
}
- 将主构造器中的
name
与id
声明为类的属性:
🏝️
class User(val name: String, val id: String) {
}
- 然后在
init
代码块中添加一些初始化逻辑:
🏝️
class User(val name: String, val id: String) {
init {
…
}
}
- 最后再添加其他次构造器:
🏝️
class User(val name: String, val id: String) {
init {
…
}
constructor(person: Person) : this(person.name, person.id) {
}
}
当一个类有多个构造器时,只需要把最基本、最通用的那个写成主构造器就行了。这里我们选择将参数为 name
与 id
的构造器作为主构造器。
到这里,整个类的初始化就完成了,类的初始化顺序就和上面的步骤一样。
除了构造器,普通函数也是有很多简化写法的。
函数简化
使用 =
连接返回值
我们已经知道了 Kotlin 中函数的写法:
🏝️
fun area(width: Int, height: Int): Int {
return width * height
}
其实,这种只有一行代码的函数,还可以这么写:
🏝️
👇
fun area(width: Int, height: Int): Int = width * height
{}
和 return
没有了,使用 =
符号连接返回值。
我们之前讲过,Kotlin 有「类型推断」的特性,那么这里函数的返回类型还可以隐藏掉:
🏝️
// 👇省略了返回类型
fun area(width: Int, height: Int) = width * height
不过,在实际开发中,还是推荐显式地将返回类型写出来,增加代码可读性。
以上是函数有返回值时的情况,对于没有返回值的情况,可以理解为返回值是 Unit
:
🏝️
fun sayHi(name: String) {
println("Hi " + name)
}
因此也可以简化成下面这样:
🏝️
👇
fun sayHi(name: String) = println("Hi " + name)
简化完函数体,我们再来看看前面的参数部分。
对于 Java 中的方法重载,我们都不陌生,那 Kolin 中是否有更方便的重载方式呢?接下来我们看看 Kotlin 中的「参数默认值」的用法。
参数默认值
Java 中,允许在一个类中定义多个名称相同的方法,但是参数的类型或个数必须不同,这就是方法的重载:
☕️
public void sayHi(String name) {
System.out.println("Hi " + name);
}
public void sayHi() {
sayHi(“world”);
}
在 Kotlin 中,也可以使用这样的方式进行函数的重载,不过还有一种更简单的方式,那就是「参数默认值」:
🏝️
👇
fun sayHi(name: String = “world”) = println("Hi " + name)
这里的 world
是参数 name
的默认值,当调用该函数时不传参数,就会使用该默认值。
这就等价于上面 Java 写的重载方法,当调用 sayHi
函数时,参数是可选的:
🏝️
sayHi(“kaixue.io”)
sayHi() // 使用了默认值 “world”
既然与重载函数的效果相同,那 Kotlin 中的参数默认值有什么好处呢?仅仅只是少写了一些代码吗?
其实在 Java 中,每个重载方法的内部实现可以各不相同,这就无法保证重载方法内部设计上的一致性,而 Kotlin 的参数默认值就解决了这个问题。
不过参数默认值在调用时也不是完全可以放飞自我的。
来看下面这段代码,这里函数中有默认值的参数在无默认值参数的前面:
🏝️
fun sayHi(name: String = “world”, age: Int) {
…
}
sayHi(10)
// 👆 这时想使用默认值进行调用,IDE 会报以下两个错误
// The integer literal does not conform to the expected type String
// No value passed for parameter ‘age’
这个错误就是告诉你参数不匹配,说明我们的「打开方式」不对,其实 Kotlin 里是通过「命名参数」来解决这个问题的。
命名参数
具体用法如下:
🏝️
fun sayHi(name: String = “world”, age: Int) {
…
}
👇
sayHi(age = 21)
在调用函数时,显式地指定了参数 age
的名称,这就是「命名参数」。Kotlin 中的每一个函数参数都可以作为命名参数。
再来看一个有非常多参数的函数的例子:
🏝️
fun sayHi(name: String = “world”, age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
…
}
当函数中有非常多的参数时,调用该函数就会写成这样:
🏝️
sayHi(“world”, 21, false, true, false)
当看到后面一长串的布尔值时,我们很难分清楚每个参数的用处,可读性很差。通过命名参数,我们就可以这么写:
🏝️
sayHi(name = “wo”, age = 21, isStudent = false, isFat = true, isTall = false)
与命名参数相对的一个概念被称为「位置参数」,也就是按位置顺序进行参数填写。
当一个函数被调用时,如果混用位置参数与命名参数,那么所有的位置参数都应该放在第一个命名参数之前:
🏝️
fun sayHi(name: String = “world”, age: Int) {
…
}
sayHi(name = “wo”, 21) // 👈 IDE 会报错,Mixing named and positioned arguments is not allowed
sayHi(“wo”, age = 21) // 👈 这是正确的写法
讲完了命名参数,我们再看看 Kotlin 中的另一种常见函数:嵌套函数。
本地函数(嵌套函数)
首先来看下这段代码,这是一个简单的登录的函数:
🏝️
fun login(user: String, password: String, illegalStr: String) {
// 验证 user 是否为空
if (user.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
// 验证 password 是否为空
if (password.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
该函数中,检查参数这个部分有些冗余,我们又不想将这段逻辑作为一个单独的函数对外暴露。这时可以使用嵌套函数,在 login
函数内部声明一个函数:
🏝️
fun login(user: String, password: String, illegalStr: String) {
👇
fun validate(value: String, illegalStr: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
👇
validate(user, illegalStr)
validate(password, illegalStr)
}
这里我们将共同的验证逻辑放进了嵌套函数 validate
中,并且 login
函数之外的其他地方无法访问这个嵌套函数。
这里的 illegalStr
是通过参数的方式传进嵌套函数中的,其实完全没有这个必要,因为嵌套函数中可以访问在它外部的所有变量或常量,例如类中的属性、当前函数中的参数与变量等。
我们稍加改进:
🏝️
fun login(user: String, password: String, illegalStr: String) {
fun validate(value: String) {
if (value.isEmpty()) {
👇
throw IllegalArgumentException(illegalStr)
}
}
…
}
这里省去了嵌套函数中的 illegalStr
参数,在该嵌套函数内直接使用外层函数 login
的参数 illegalStr
。
上面 login
函数中的验证逻辑,其实还有另一种更简单的方式:
🏝️
fun login(user: String, password: String, illegalStr: String) {
require(user.isNotEmpty()) { illegalStr }
require(password.isNotEmpty()) { illegalStr }
}
其中用到了 lambda 表达式以及 Kotlin 内置的 require
函数,这里先不做展开,之后的文章会介绍。
字符串
讲完了普通函数的简化写法,Kotlin 中字符串也有很多方便写法。
字符串模板
在 Java 中,字符串与变量之间是使用 +
符号进行拼接的,Kotlin 中也是如此:
🏝️
val name = “world”
println("Hi " + name)
但是当变量比较多的时候,可读性会变差,写起来也比较麻烦。
Java 给出的解决方案是 String.format
:
☕️
System.out.print(String.format(“Hi %s”, name));
Kotlin 为我们提供了一种更加方便的写法:
🏝️
val name = “world”
// 👇 用 ‘$’ 符号加参数的方式
println(“Hi $name”)
这种方式就是把 name
从后置改为前置,简化代码的同时增加了字符串的可读性。
除了变量,$
后还可以跟表达式,但表达式是一个整体,所以我们要用 {}
给它包起来:
🏝️
val name = “world”
println(“Hi ${name.length}”)
其实就跟四则运算的括号一样,提高语法上的优先级,而单个变量的场景可以省略 {}
。
字符串模板还支持转义字符,比如使用转义字符 \n
进行换行操作:
🏝️
val name = “world!\n”
println(“Hi $name”) // 👈 会多打一个空行
字符串模板的用法对于我们 Android 工程师来说,其实一点都不陌生。
首先,Gradle 所用的 Groovy 语言就已经有了这种支持:
def name = “world”
println “Hi ${name}”
在 Android 的资源文件里,定义字符串也有类似用法:
Hi %s
☕️
getString(R.id.hi, “world”);
raw string (原生字符串)
有时候我们不希望写过多的转义字符,这种情况 Kotlin 通过「原生字符串」来实现。
用法就是使用一对 """
将字符串括起来:
🏝️
val name = “world”
val myName = “kotlin”
👇
val text = “”"
Hi $name!
My name is $myName.\n
“”"
println(text)
这里有几个注意点:
\n
并不会被转义- 最后输出的内容与写的内容完全一致,包括实际的换行
$
符号引用变量仍然生效
这就是「原生字符串」。输出结果如下:
Hi world!
My name is kotlin.\n
但对齐方式看起来不太优雅,原生字符串还可以通过 trimMargin()
函数去除每行前面的空格:
🏝️
val text = “”"
👇
|Hi world!
|My name is kotlin.
“”".trimMargin()
println(text)
输出结果如下:
Hi world!
My name is kotlin.
这里的 trimMargin()
函数有以下几个注意点:
|
符号为默认的边界前缀,前面只能有空格,否则不会生效- 输出时
|
符号以及它前面的空格都会被删除 - 边界前缀还可以使用其他字符,比如
trimMargin("/")
,只不过上方的代码使用的是参数默认值的调用方式
字符串的部分就先到这里,下面来看看数组与集合有哪些更方便的操作。
数组和集合
数组与集合的操作符
在之前的文章中,我们已经知道了数组和集合的基本概念,其实 Kotlin 中,还为我们提供了许多使数组与集合操作起来更加方便的函数。
首先声明如下 IntArray
和 List
:
🏝️
val intArray = intArrayOf(1, 2, 3)
val strList = listOf(“a”, “b”, “c”)
接下来,对它们的操作函数进行讲解:
forEach
:遍历每一个元素
🏝️
// 👇 lambda 表达式,i 表示数组的每个元素
intArray.forEach { i ->
print(i + " ")
}
// 输出:1 2 3
除了「lambda」表达式,这里也用到了「闭包」的概念,这又是另一个话题了,这里先不展开。
filter
:对每个元素进行过滤操作,如果 lambda 表达式中的条件成立则留下该元素,否则剔除,最终生成新的集合
🏝️
// [1, 2, 3]
⬇️
// {2, 3}
// 👇 注意,这里变成了 List
val newList: List = intArray.filter { i ->
i != 1 // 👈 过滤掉数组中等于 1 的元素
}
map
:遍历每个元素并执行给定表达式,最终形成新的集合
🏝️
// [1, 2, 3]
⬇️
// {2, 3, 4}
val newList: List = intArray.map { i ->
i + 1 // 👈 每个元素加 1
}
flatMap
:遍历每个元素,并为每个元素创建新的集合,最后合并到一个集合中
🏝️
// [1, 2, 3]
⬇️
// {“2”, “a” , “3”, “a”, “4”, “a”}
intArray.flatMap { i ->
listOf(“${i + 1}”, “a”) // 👈 生成新集合
}
关于为什么数组的 filter
之后变成 List
,就留作思考题吧~
这里是以数组 intArray
为例,集合 strList
也同样有这些操作函数。Kotlin 中还有许多类似的操作函数,这里就不一一列举了。
除了数组和集合,Kotlin 中还有另一种常用的数据类型: Range
。
Range
在 Java 语言中并没有 Range
的概念,Kotlin 中的 Range
表示区间的意思,也就是范围。区间的常见写法如下:
🏝️
👇 👇
val range: IntRange = 0…1000
这里的 0..1000
就表示从 0 到 1000 的范围,包括 1000,数学上称为闭区间 [0, 1000]。除了这里的 IntRange
,还有 CharRange
以及 LongRange
。
Kotlin 中没有纯的开区间的定义,不过有半开区间的定义:
🏝️
👇
val range: IntRange = 0 until 1000
这里的 0 until 1000
表示从 0 到 1000,但不包括 1000,这就是半开区间 [0, 1000) 。
Range
这个东西,天生就是用来遍历的:
🏝️
val range = 0…1000
// 👇 默认步长为 1,输出:0, 1, 2, 3, 4, 5, 6, 7…1000,
for (i in range) {
print("$i, ")
}
这里的 in
关键字可以与 for
循环结合使用,表示挨个遍历 range
中的值。关于 for
循环控制的使用,在本期文章的后面会做具体讲解。
除了使用默认的步长 1,还可以通过 step
设置步长:
🏝️
val range = 0…1000
// 👇 步长为 2,输出:0, 2, 4, 6, 8, 10,…1000,
for (i in range step 2) {
print("$i, ")
}
以上是递增区间,Kotlin 还提供了递减区间 downTo
,不过递减没有半开区间的用法:
🏝️
// 👇 输出:4, 3, 2, 1,
for (i in 4 downTo 1) {
print("$i, ")
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
结尾
如何才能让我们在面试中对答如流呢?
答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?为此我整理了一份Android学习资料路线:
这里是一份BAT大厂面试资料专题包:
好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。
中…(img-u0ex0N6r-1712043851600)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-jGWtSG1V-1712043851601)]
结尾
如何才能让我们在面试中对答如流呢?
答案当然是平时在工作或者学习中多提升自身实力的啦,那如何才能正确的学习,有方向的学习呢?为此我整理了一份Android学习资料路线:
[外链图片转存中…(img-lseX6VnH-1712043851601)]
这里是一份BAT大厂面试资料专题包:
[外链图片转存中…(img-vZJTlFtN-1712043851601)]
好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划。来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。