本期做者:java
视频:扔物线(朱凯)git
文章:Walker(张磊)github
你们好,我是扔物线朱凯。这是码上开学 Kotlin 基础部分的第二期:Kotlin 里那些「不是那么写的」。话很少说,视频伺候。编程
由于我一直没有学会怎么在掘金贴视频,因此请点击 这里 去哔哩哔哩看,或者点击 这里 去 YouTube 看。数组
如下内容来自文章做者 Walker。安全
上一篇咱们讲了 Kotlin 上手最基础的三个点:变量、函数和类型。你们都据说过,Kotlin 彻底兼容 Java,这个意思是用 Java 写出来的代码和 Kotlin 能够完美交互,而不是说你用 Java 的写法去写 Kotlin 就彻底没问题,这个是不行的。这期内容咱们就讲一下,Kotlin 里那些「不 Java」的写法。性能优化
上一篇中简单介绍了 Kotlin 的构造器,这一节具体看看 Kotlin 的构造器和 Java 有什么不同的地方:数据结构
Java架构
☕️
public class User {
int id;
String name;
👇 👇
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
复制代码
Kotlinide
🏝️
class User {
val id: Int
val name: String
👇
constructor(id: Int, name: String) {
//👆 没有 public
this.id = id
this.name = name
}
}
复制代码
能够发现有两点不一样:
constructor
表示。除了构造器,Java 里经常配合一块儿使用的 init 代码块,在 Kotlin 里的写法也有了一点点改变:你须要给它加一个 init
前缀。
Java
☕️
public class User {
👇
{
// 初始化代码块,先于下面的构造器执行
}
public User() {
}
}
复制代码
Kotlin
🏝️
class User {
👇
init {
// 初始化代码块,先于下面的构造器执行
}
constructor() {
}
}
复制代码
正如上面标注的那样,Kotlin 的 init 代码块和 Java 同样,都在实例化时执行,而且执行顺序都在构造器以前。
上一篇提到,Java 的类若是不加 final 关键字,默认是能够被继承的,而 Kotlin 的类默认就是 final 的。在 Java 里 final 还能够用来修饰变量,接下来让咱们看看 Kotlin 是如何实现相似功能的。
final
Kotlin 中的 val
和 Java 中的 final
相似,表示只读变量,不能修改。这里分别从成员变量、参数和局部变量来和 Java 作对比:
Java
☕️
👇
final int final1 = 1;
👇
void method(final String final2) {
👇
final String final3 = "The parameter is " + final2;
}
复制代码
Kotlin
🏝️
👇
val fina1 = 1
// 👇 参数是没有 val 的
fun method(final2: String) {
👇
val final3 = "The parameter is " + final2
}
复制代码
能够看到不一样点主要有:
上一期说过,var
是 variable 的缩写, val
是 value 的缩写。
其实咱们写 Java 代码的时候,不多会有人用 final
,但 final
用来修饰变量实际上是颇有用的,但你们都不用;可你若是去看看国内国外的人写的 Kotlin 代码,你会发现不少人的代码里都会有一堆的 val
。为何?由于 final
写起来比 val
麻烦一点:我须要多写一个单词。虽然只麻烦这一点点,但就致使不少人不写。
这就是一件颇有意思的事:从 final
到 val
,只是方便了一点点,但却让它的使用频率有了巨大的改变。这种改变是会影响到代码质量的:在该加限制的地方加上限制,就能够减小代码出错的几率。
val
自定义 getter不过 val
和 final
仍是有一点区别的,虽然 val
修饰的变量不能二次赋值,但能够经过自定义变量的 getter 函数,让变量每次被访问时,返回动态获取的值:
🏝️
👇
val size: Int
get() { // 👈 每次获取 size 值时都会执行 items.size
return items.size
}
复制代码
不过这个属于 val
的另一种用法,大部分状况下 val
仍是对应于 Java 中的 final
使用的。
static
property / function刚才说到你们都不喜欢写 final
对吧?但有一种场景,你们是最喜欢用 final
的:常量。
☕️
public static final String CONST_STRING = "A String";
复制代码
在 Java 里面写常量,咱们用的是 static
+ final
。而在 Kotlin 里面,除了 final
的写法不同,static
的写法也不同,并且是更不同。确切地说:在 Kotlin
里,静态变量和静态方法这两个概念被去除了。
那若是想在 Kotlin 中像 Java 同样经过类直接引用该怎么办呢?Kotlin 的答案是 companion object
:
🏝️
class Sample {
...
👇
companion object {
val anotherString = "Another String"
}
}
复制代码
为啥 Kotlin 越改越复杂了?不着急,咱们先看看 object
是个什么东西。
object
Kotlin 里的 object
——首字母小写的,不是大写,Java 里的 Object
在 Kotlin 里不用了。
Java 中的
Object
在 Kotlin 中变成了Any
,和Object
做用同样:做为全部类的基类。
而 object
不是类,像 class
同样在 Kotlin 中属于关键字:
🏝️
object Sample {
val name = "A name"
}
复制代码
它的意思很直接:建立一个类,而且建立一个这个类的对象。这个就是 object
的意思:对象。
在代码中若是要使用这个对象,直接经过它的类名就能够访问:
🏝️
Sample.name
复制代码
这不就是单例么,因此在 Kotlin 中建立单例不用像 Java 中那么复杂,只须要把 class
换成 object
就能够了。
单例类
咱们看一个单例的例子,分别用 Java 和 Kotlin 实现:
Java 中实现单例类(非线程安全):
☕️
public class A {
private static A sInstance;
public static A getInstance() {
if (sInstance == null) {
sInstance = new A();
}
return sInstance;
}
// 👇还有不少模板代码
...
}
复制代码
能够看到 Java 中为了实现单例类写了大量的模版代码,稍显繁琐。
Kotlin 中实现单例类:
🏝️
// 👇 class 替换成了 object
object A {
val number: Int = 1
fun method() {
println("A.method()")
}
}
复制代码
和 Java 相比的不一样点有:
class
换成了 object
。sInstance
。getInstance()
方法。相比 Java 的实现简单多了。
这种经过
object
实现的单例是一个饿汉式的单例,而且实现了线程安全。
继承类和实现接口
Kotlin 中不只类能够继承别的类,能够实现接口,object
也能够:
🏝️
open class A {
open fun method() {
...
}
}
interface B {
fun interfaceMethod()
}
👇 👇 👇
object C : A(), B {
override fun method() {
...
}
override fun interfaceMethod() {
...
}
}
复制代码
为何 object 能够实现接口呢?简单来说 object 实际上是把两步合并成了一步,既有 class 关键字的功能,又实现了单例,这样就容易理解了。
匿名类
另外,Kotlin 还能够建立 Java 中的匿名类,只是写法上有点不一样:
Java:
☕️ 👇
ViewPager.SimpleOnPageChangeListener listener = new ViewPager.SimpleOnPageChangeListener() {
@Override // 👈
public void onPageSelected(int position) {
// override
}
};
复制代码
Kotlin:
🏝️
val listener = object: ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
// override
}
}
复制代码
和 Java 建立匿名类的方式很类似,只不过把 new
换成了 object:
:
new
用来建立一个匿名类的对象object:
也能够用来建立匿名类的对象这里的 new
和 object:
修饰的都是接口或者抽象类。
companion object
用 object
修饰的对象中的变量和函数都是静态的,但有时候,咱们只想让类中的一部分函数和变量是静态的该怎么作呢:
🏝️
class A {
👇
object B {
var c: Int = 0
}
}
复制代码
如上,能够在类中建立一个对象,把须要静态的变量或函数放在内部对象 B 中,外部能够经过以下的方式调用该静态变量:
🏝️
A.B.c
👆
复制代码
类中嵌套的对象能够用 companion
修饰:
🏝️
class A {
👇
companion object B {
var c: Int = 0
}
}
复制代码
companion
能够理解为伴随、伴生,表示修饰的对象和外部类绑定。
但这里有一个小限制:一个类中最多只能够有一个伴生对象,但能够有多个嵌套对象。就像皇帝后宫佳丽三千,但皇后只有一个。
这样的好处是调用的时候能够省掉对象名:
🏝️
A.c // 👈 B 没了
复制代码
因此,当有 companion
修饰时,对象的名字也能够省略掉:
🏝️
class A {
// 👇 B 没了
companion object {
var c: Int = 0
}
}
复制代码
这就是这节最开始讲到的,Java 静态变量和方法的等价写法:companion object
变量和函数。
静态初始化
Java 中的静态变量和方法,在 Kotlin 中都放在了 companion object
中。所以 Java 中的静态初始化在 Kotlin 中天然也是放在 companion object
中的,像类的初始化代码同样,由 init
和一对大括号表示:
🏝️
class Sample {
👇
companion object {
👇
init {
...
}
}
}
复制代码
除了静态函数这种简便的调用方式,Kotlin 还有更方便的东西:「top-level declaration
顶层声明」。其实就是把属性和函数的声明不写在 class
里面,这个在 Kotlin 里是容许的:
🏝️
package com.hencoder.plus
// 👇 属于 package,不在 class/object 内
fun topLevelFuncion() {
}
复制代码
这样写的属性和函数,不属于任何 class
,而是直接属于 package
,它和静态变量、静态函数同样是全局的,但用起来更方便:你在其它地方用的时候,就连类名都不用写:
🏝️
import com.hencoder.plus.topLevelFunction // 👈 直接 import 函数
topLevelFunction()
复制代码
写在顶级的函数或者变量有个好处:在 Android Studio 中写代码时,IDE 很容易根据你写的函数前几个字母自动联想出相应的函数。这样提升了写代码的效率,并且能够减小项目中的重复代码。
命名相同的顶级函数
顶级函数不写在类中可能有一个问题:若是在不一样文件中声明命名相同的函数,使用的时候会不会混淆?来看一个例子:
在 org.kotlinmaster.library
包下有一个函数 method:
🏝️
package org.kotlinmaster.library1
👆
fun method() {
println("library1 method()")
}
复制代码
在 org.kotlinmaster.library2
包下有一个同名函数:
🏝️
package org.kotlinmaster.library2
👆
fun method() {
println("library2 method()")
}
复制代码
在使用的时候若是同时调用这两个同名函数会怎么样:
🏝️
import org.kotlinmaster.library1.method
👆
fun test() {
method()
👇
org.kotlinmaster.library2.method()
}
复制代码
能够看到当出现两个同名顶级函数时,IDE 会自动加上包前缀来区分,这也印证了「顶级函数属于包」的特性。
那在实际使用中,在 object
、companion object
和 top-level 中该选择哪个呢?简单来讲按照下面这两个原则判断:
object
或 companion object
。Java 中,除了上面讲到的的静态变量和方法会用到 static
,声明常量时也会用到,那 Kotlin 中声明常量会有什么变化呢?
Java 中声明常量:
☕️
public class Sample {
👇 👇
public static final int CONST_NUMBER = 1;
}
复制代码
Kotlin 中声明常量:
🏝️
class Sample {
companion object {
👇 // 👇
const val CONST_NUMBER = 1
}
}
const val CONST_SECOND_NUMBER = 2
复制代码
发现不一样点有:
const
关键字。除此以外还有一个区别:
缘由是 Kotlin 中的常量指的是 「compile-time constant 编译时常量」,它的意思是「编译器在编译的时候就知道这个东西在每一个调用处的实际值」,所以能够在编译时直接把这个值硬编码到代码里使用的地方。
而非基本和 String 类型的变量,能够经过调用对象的方法或变量改变对象内部的值,这样这个变量就不是常量了,来看一个 Java 的例子,好比一个 User 类:
☕️
public class User {
int id; // 👈 可修改
String name; // 👈 可修改
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
复制代码
在使用的地方声明一个 static final
的 User 实例 user
,它是不能二次赋值的:
☕️
static final User user = new User(123, "Zhangsan");
👆 👆
复制代码
可是能够经过访问这个 user
实例的成员变量改变它的值:
☕️
user.name = "Lisi";
👆
复制代码
因此 Java 中的常量能够认为是「伪常量」,由于能够经过上面这种方式改变它内部的值。而 Kotlin 的常量由于限制类型必须是基本类型,因此不存在这种问题,更符合常量的定义。
前面讲的 val
「只读变量」和静态变量都是针对单个变量来讲的,接下来咱们看看编程中另一个常见的主题:数组和集合。
声明一个 String 数组:
Java 中的写法:
☕️
String[] strs = {"a", "b", "c"};
👆 👆
复制代码
Kotlin 中的写法:
🏝️
val strs: Array<String> = arrayOf("a", "b", "c")
👆 👆
复制代码
能够看到 Kotlin 中的数组是一个拥有泛型的类,建立函数也是泛型函数,和集合数据类型同样。
针对泛型的知识点,咱们在后面的文章会讲,这里就先按照 Java 泛型来理解。
将数组泛型化有什么好处呢?对数组的操做能够像集合同样功能更强大,因为泛型化,Kotlin 能够给数组增长不少有用的工具函数:
get() / set()
contains()
first()
find()
这样数组的实用性就大大增长了。
取值和修改
Kotlin 中获取或者设置数组元素和 Java 同样,可使用方括号加下标的方式索引:
🏝️
println(strs[0])
👇 👆
strs[1] = "B"
复制代码
不支持协变
Kotlin 的数组编译成字节码时使用的仍然是 Java 的数组,但在语言层面是泛型实现,这样会失去协变 (covariance) 特性,就是子类数组对象不能赋值给父类的数组变量:
Kotlin
🏝️
val strs: Array<String> = arrayOf("a", "b", "c")
👆
val anys: Array<Any> = strs // compile-error: Type mismatch
👆
复制代码
而这在 Java 中是能够的:
☕️
String[] strs = {"a", "b", "c"};
👆
Object[] objs = strs; // success
👆
复制代码
关于协变的问题,这里就先不展开了,后面讲泛型的时候会提到。
Kotlin 和 Java 同样有三种集合类型:List、Set 和 Map,它们的含义分别以下:
List
以固定顺序存储一组元素,元素能够重复。Set
存储一组互不相等的元素,一般没有固定顺序。Map
存储 键-值 对的数据集合,键互不相等,但不一样的键能够对应相同的值。从 Java 到 Kotlin,这三种集合类型的使用有哪些变化呢?咱们依次看看。
List
Java 中建立一个列表:
☕️
List<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");
strList.add("c"); // 👈 添加元素繁琐
复制代码
Kotlin 中建立一个列表:
🏝️
val strList = listOf("a", "b", "c")
复制代码
首先能看到的是 Kotlin 中建立一个 List
特别的简单,有点像建立数组的代码。并且 Kotlin 中的 List
多了一个特性:支持 covariant(协变)。也就是说,能够把子类的 List
赋值给父类的 List
变量:
Kotlin:
🏝️
val strs: List<String> = listOf("a", "b", "c")
👆
val anys: List<Any> = strs // success
👆
复制代码
而这在 Java 中是会报错的:
☕️
List<String> strList = new ArrayList<>();
👆
List<Object> objList = strList; // 👈 compile error: incompatible types
👆
复制代码
对于协变的支持与否,List
和数组恰好反过来了。关于协变,这里只需结合例子简单了解下,后面的文章会对它展开讨论。
和数组的区别
Kotlin 中数组和 MutableList 的 API 是很是像的,主要的区别是数组的元素个数不能变。那在何时用数组呢?
这个问题在 Java 中就存在了,数组和 List
的功能相似,List
的功能更多一些,直觉上应该用 List
。但数组也不是没有优点,基本类型 (int[]
、float[]
) 的数组不用自动装箱,性能好一点。
在 Kotlin 中也是一样的道理,在一些性能需求比较苛刻的场景,而且元素类型是基本类型时,用数组好一点。不过这里要注意一点,Kotlin 中要用专门的基本类型数组类 (IntArray
FloatArray
LongArray
) 才能够免于装箱。也就是说元素不是基本类型时,相比 Array
,用 List
更方便些。
Set
Java 中建立一个 Set
:
☕️
Set<String> strSet = new HashSet<>();
strSet.add("a");
strSet.add("b");
strSet.add("c");
复制代码
Kotlin 中建立相同的 Set
:
🏝️
val strSet = setOf("a", "b", "c")
复制代码
和 List
相似,Set
一样具备 covariant(协变)特性。
Map
Java 中建立一个 Map
:
☕️
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
map.put("key4", 3);
复制代码
Kotlin 中建立一个 Map
:
🏝️
val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3)
复制代码
和上面两种集合类型类似建立代码很简洁。mapOf
的每一个参数表示一个键值对,to
表示将「键」和「值」关联,这个叫作「中缀表达式」,这里先不展开,后面的文章会作介绍。
取值和修改
Kotlin 中的 Map 除了和 Java 同样可使用 get()
根据键获取对应的值,还可使用方括号的方式获取:
🏝️
👇
val value1 = map.get("key1")
👇
val value2 = map["key2"]
复制代码
相似的,Kotlin 中也能够用方括号的方式改变 Map
中键对应的值:
🏝️
👇
val map = mutableMapOf("key1" to 1, "key2" to 2)
👇
map.put("key1", 2)
👇
map["key1"] = 2
复制代码
这里用到了「操做符重载」的知识,实现了和数组同样的「Positional Access Operations」,关于这个概念这里先不展开,后面会讲到。
可变集合/不可变集合
上面修改 Map
值的例子中,建立函数用的是 mutableMapOf()
而不是 mapOf()
,由于只有 mutableMapOf()
建立的 Map
才能够修改。Kotlin 中集合分为两种类型:只读的和可变的。这里的只读有两层意思:
如下是三种集合类型建立不可变和可变实例的例子:
listOf()
建立不可变的 List
,mutableListOf()
建立可变的 List
。setOf()
建立不可变的 Set
,mutableSetOf()
建立可变的 Set
。mapOf()
建立不可变的 Map
,mutableMapOf()
建立可变的 Map
。能够看到,有 mutable
前缀的函数建立的是可变的集合,没有 mutbale
前缀的建立的是不可变的集合,不过不可变的能够经过 toMutable*()
系函数转换成可变的集合:
🏝️
val strList = listOf("a", "b", "c")
👇
strList.toMutableList()
val strSet = setOf("a", "b", "c")
👇
strSet.toMutableSet()
val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3)
👇
map.toMutableMap()
复制代码
而后就能够对集合进行修改了,这里有一点须要注意下:
toMutable*()
返回的是一个新建的集合,原有的集合仍是不可变的,因此只能对函数返回的集合修改。Sequence
除了集合 Kotlin 还引入了一个新的容器类型 Sequence
,它和 Iterable
同样用来遍历一组数据并能够对每一个元素进行特定的处理,先来看看如何建立一个 Sequence
。
相似 listOf()
,使用一组元素建立:
🏝️
sequenceOf("a", "b", "c")
复制代码
使用 Iterable
建立:
🏝️
val list = listOf("a", "b", "c")
list.asSequence()
复制代码
这里的 List
实现了 Iterable
接口。
使用 lambda 表达式建立:
🏝️ // 👇 第一个元素
val sequence = generateSequence(0) { it + 1 }
// 👆 lambda 表达式,负责生成第二个及之后的元素,it 表示前一个元素
复制代码
这看起来和 Iterable
同样呀,为啥要画蛇添足使用 Sequence
呢?在下一篇文章中会结合例子展开讨论。
讲完了数据集合,再看看 Kotlin 中的可见性修饰符,Kotlin 中有四种可见性修饰符:
public
:公开,可见性最大,哪里均可以引用。private
:私有,可见性最小,根据声明位置不一样可分为类中可见和文件中可见。protected
:保护,至关于 private
+ 子类可见。internal
:内部,仅对 module 内可见。相比 Java 少了一个 default
「包内可见」修饰符,多了一个 internal
「module 内可见」修饰符。这一节结合例子讲讲 Kotlin 这四种可见性修饰符,以及在 Kotlin 和 Java 中的不一样。先来看看 public
:
public
Java 中没写可见性修饰符时,表示包内可见,只有在同一个 package
内能够引用:
☕️ 👇
package org.kotlinmaster.library;
// 没有可见性修饰符
class User {
}
复制代码
☕️ // 👇 和上面同一个 package
package org.kotlinmaster.library;
public class Example {
void method() {
new User(); // success
}
}
复制代码
☕️
package org.kotlinmaster;
// 👆 和上面不是一个 package
import org.kotlinmaster.library.User;
👆
public class OtherPackageExample {
void method() {
new User(); // compile-error: 'org.kotlinmaster.library.User' is not public in 'org.kotlinmaster.library'. Cannot be accessed from outside package
}
}
复制代码
package
外若是要引用,须要在 class
前加上可见性修饰符 public
表示公开。
Kotlin 中若是不写可见性修饰符,就表示公开,和 Java 中 public
修饰符具备相同效果。在 Kotlin 中 public
修饰符「能够加,但不必」。
@hide
在 Android 的官方 sdk 中,有一些方法只想对 sdk 内可见,不想开放给用户使用(由于这些方法不太稳定,在后续版本中颇有可能会修改或删掉)。为了实现这个特性,会在方法的注释中添加一个 Javadoc 方法 @hide
,用来限制客户端访问:
☕️
/** * @hide 👈 */
public void hideMethod() {
...
}
复制代码
但这种限制不太严格,能够经过反射访问到限制的方法。针对这个状况,Kotlin 引进了一个更为严格的可见性修饰符:internal
。
internal
internal
表示修饰的类、函数仅对 module 内可见,这里的 module 具体指的是一组共同编译的 kotlin 文件,常见的形式有:
咱们常见的是 Android Studio 中的 module 这种状况,Maven project 仅做了解就好,不用细究。
internal
在写一个 library module 时很是有用,当须要建立一个函数仅开放给 module 内部使用,不想对 library 的使用者可见,这时就应该用 internal
可见性修饰符。
Java 的 default
「包内可见」在 Kotlin 中被弃用掉了,Kotlin 中与它最接近的可见性修饰符是 internal
「module 内可见」。为何会弃用掉包内可见呢?我以为有这几个缘由:
internal
「module 内可见」已经能够知足对于代码封装的需求。protected
protected
表示包内可见 + 子类可见。protected
表示 private
+ 子类可见。Kotlin 相比 Java protected
的可见范围收窄了,缘由是 Kotlin 中再也不有「包内可见」的概念了,相比 Java 的可见性着眼于 package
,Kotlin 更关心的是 module。
private
private
表示类中可见,做为内部类时对外部类「可见」。private
表示类中或所在文件内可见,做为内部类时对外部类「不可见」。private
修饰的变量「类中可见」和 「文件中可见」:
🏝️
class Sample {
private val propertyInClass = 1 // 👈 仅 Sample 类中可见
}
private val propertyInFile = "A string." // 👈 范围更大,整个文件可见
复制代码
private
修饰内部类的变量时,在 Java 和 Kotlin 中的区别:
在 Java 中,外部类能够访问内部类的 private
变量:
☕️
public class Outter {
public void method() {
Inner inner = new Inner();
👇
int result = inner.number * 2; // success
}
private class Inner {
private int number = 0;
}
}
复制代码
在 Kotlin 中,外部类不能够访问内部类的 private
变量:
🏝️
class Outter {
fun method() {
val inner = Inner()
👇
val result = inner.number * 2 // compile-error: Cannot access 'number': it is private in 'Inner'
}
class Inner {
private val number = 1
}
}
复制代码
能够修饰类和接口
Java 中一个文件只容许一个外部类,因此 class
和 interface
不容许设置为 private
,由于声明 private
后没法被外部使用,这样就没有意义了。
Kotlin 容许同一个文件声明多个 class
和 top-level 的函数和属性,因此 Kotlin 中容许类和接口声明为 private
,由于同个文件中的其它成员能够访问:
🏝️
private class Sample {
val number = 1
fun method() {
println("Sample method()")
}
}
// 👇 在同一个文件中,因此能够访问
val sample = Sample()
复制代码
Walker(张磊) ,即刻 Android 高级工程师。2015 年加入即刻,参与了即刻 2.0 到 6.0 版本的架构设计和产品迭代。多年 Android 开发经验,曾就任于 OPPO,专一于客户端用户体验、音视频开发和性能优化。