Kotlin中的琐碎细节要点

339 阅读12分钟

在kotlin语言的研发中,也会有一些基本的语言特点需要理解和掌握,本文就整理一些看似琐碎但开发中也很重要的技术点。
1,for循环时的边界(until 与 ..)
Kotlin 的 until 操作符生成的是左闭右开的范围(即 [start, end))
比如,1 < i < 10 可以书写为 for (i in 2 until 10) {}
1 <= i < 10 可以书写为 for (i in 1 until 10) {}
Kotlin 的 .. 操作符生成的是左闭右闭的范围(即 [start, end])
1 < i <= 10可以书写为 for (i in 2..10) {}

2,引用当前活动的上下文 使用 this@MyActivity 来引用当前活动的上下文,这是 Kotlin 中的一种语法,用于在扩展函数或内部类中引用外部类的实例。

3,使用!!操作符强制解包的风险
对于一个变量pageCount,如果它是一个可能为null的类型,用!!操作符是用来强制解包这个可能为null的值的,意味着如果pageCount是null,那么程序将会在这里抛出NullPointerException异常。
更安全的做法是使用let函数处理非空值,或者使用?:操作符提供一个默认值,或者使用when表达式等来处理可能的null情况。

val pageCount = viewPager.adapter?.count
pageCount?.let {
	for (i in 0 until it) {
		...
	}
}

要优于下面的写法:

for (i in 0 until pageCount!!) {}

4,启动新的activity
在java文件中,启动一个activity可以如下启动:

Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);

在kotlin文件中,就需要这样启动:

val intent = Intent(this, MainActivity::class.java)
startActivity(intent)

从上面代码可以看出,
在Kotlin中,当想要获取一个Kotlin类的Java Class对象时,你需要在类引用后面使用::class.java。
因为Kotlin和Java在运行时使用不同的类型表示系统,而Android框架的许多部分(包括Intent)都是基于Java的Class对象来工作的。
::class是Kotlin的一个特性,它提供了一个指向类的KClass类型引用的方式。
但是,由于Android API(如Intent的构造函数)需要Java的Class<?>类型,你需要通过.java属性从KClass获取对应的Java Class对象。
所以在上面的代码中, MainActivity::class 是一个指向MainActivity类的KClass类型的引用。
.java 是将这个KClass引用转换为Java的Class类型。
由此可知,::class.java在Kotlin中用于:
获取Kotlin类的Java Class对象。
允许Kotlin代码与基于Java API的Android框架进行交互。

5,对Java匿名内部类的实现
在Java中,匿名内部类是没有名称的内部类,它允许您直接实例化一个类并同时重写其方法,而无需为该类提供一个明确的名称。

private final View.OnTouchListener viewTouchListener = new View.OnTouchListener() {
        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            Log.v(TAG, "viewTouchListener...onTouch...");
            return false;
        }
    }

上面的Java代码段就是一个匿名内部类的实现。
如果采用非匿名内部类的实现方式,则需要定义一个单独的类来实现View.OnTouchListener接口,并在需要的地方实例化这个类。

public class MyTouchListener implements View.OnTouchListener {
    private static final String TAG = "MyTouchListener";
 
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.v(TAG, "viewTouchListener...onTouch...");
		return false;
	}
}

在活动或片段中,可以这样实例化并使用这个类:

private final View.OnTouchListener viewTouchListener = new MyTouchListener();

对于上面的代码,在kotlin中的实现如下:

private val touchListener = object : View.OnTouchListener {
	@SuppressLint("ClickableViewAccessibility")
	override fun onTouch(v: View?, event: MotionEvent?): Boolean {
		return false
	}
}

上面的代码通过objects采用匿名内部类的方式实现。
object : View.OnTouchListener 创建了一个匿名内部类的实例,该实例实现了View.OnTouchListener接口。
当然,也可以采用Lambda的方式实现。

@SuppressLint("ClickableViewAccessibility")
private val touchLambda = View.OnTouchListener {
	v,event ->
	Log.v(TAG, "touchLambda...")
	false
}

也可以写成如下:

@SuppressLint("ClickableViewAccessibility")
private val touchLambda = View.OnTouchListener {
    v, _ ->
    Log.v(TAG, "touchLambda...")
    false
}

在Kotlin中,下划线 _ 用作一个特殊的标识符,表示一个未使用的参数。当您编写一个lambda表达式或函数,并且某个参数在函数体内不会被用到时,可以使用下划线来代替参数名,这样可以使代码更加简洁清晰。
在上面的代码中 View.OnTouchListener接口中的onTouch方法原本有两个参数:v(触发事件的视图)和event(触摸事件的信息)。
然而,在这个lambda表达式中,只关心触发事件的视图v,而不关心触摸事件的具体信息event。因此,使用下划线_来代替event的参数名,以明确表示不会在这个lambda表达式中使用它。
这样做的好处是: 提高代码可读性,可以一眼看出哪些参数是未使用的。 避免编译器警告,即如果某个参数在函数体内未被使用,编译器可能会发出警告。使用下划线可以避免这种警告。
综合上面代码进一步分析匿名内部类和lambda表达式:
在Kotlin中,匿名内部类和lambda表达式都是用于实现接口或抽象类的便捷方式。
匿名内部类可以包含多个方法实现、构造函数、初始化块等,而lambda表达式只能实现单个方法(对于函数式接口)。 在很多情况下,lambda表达式和匿名内部类可以互换使用,但选择哪种方式取决于接口的复杂性、代码的可读性。
Kotlin编译器会将lambda表达式转换为适当的匿名内部类(或类似的结构),以便在Java字节码中表示。

6,注册接收器 在java文件中,

private void registerHelloReceiver() {
	IntentFilter filter = new IntentFilter();
	filter.addAction(“hello”);
	registerReceiver(helloReceiver, filter, Context.RECEIVER_EXPORTED);
}

在kotlin文件中

private fun registerHelloReceiver() {
    val filter = IntentFilter().apply {
        addAction("hello") //或this.addAction("hello")
    }
    registerReceiver(helloReceiver, filter, Context.RECEIVER_EXPORTED)
}

在上面的代码中,使用apply函数来链式调用IntentFilter的addAction方法。这允许我们在一行内设置过滤器,同时保持代码的清晰和可读性。
在Kotlin中,apply函数是一个非常实用的标准库函数,它定义在Kotlin.Extensions中的Any类上,这意味着它可以被任何对象调用。
apply函数接收一个lambda表达式作为参数,在这个lambda表达式内部,你可以通过this关键字(或者省略this,直接使用对象的属性和方法)来访问和修改调用apply的对象。
apply函数执行完毕后,会返回调用它的对象本身,这使得它非常适合用于链式调用。
链式调用(Chaining)是一种编程技术,允许通过单个对象连续调用其方法,每个方法返回的对象支持后续方法的调用。
在链式调用中,每个方法通常会返回调用它的对象本身(即 this),或者返回另一个支持相同方法调用的对象。这样,你就可以将多个方法调用串联在一起,形成一个“链”。
apply函数提供了一种简洁的方式来设置对象的多个属性或执行多个操作,而不需要创建临时变量或编写额外的代码。
apply函数的使用场景:
初始化对象:当你需要初始化一个对象并设置其多个属性时,apply函数可以让你的代码更加简洁和易读。
链式调用:由于apply函数返回调用对象本身,因此你可以将多个apply调用链接在一起,以设置对象的多个属性或执行多个操作。
在扩展函数中修改对象:当你编写一个扩展函数,并希望在该函数中修改接收者对象时,apply函数是一个很好的选择。

Kotlin还提供了另一个类似的函数with,它也可以用于设置对象的多个属性。
with函数接收两个参数:一个对象和一个lambda表达式。在lambda表达式内部,你可以通过this关键字(或者在Kotlin中更常见的是省略this直接调用方法和属性)来访问该对象的成员。
with函数不返回调用它的对象,而是返回lambda表达式的返回值。因此,with函数通常用于当你不需要链式调用,但想要在一个作用域内访问和修改对象时。
在Kotlin的with函数中,你不能直接使用it作为隐含的接收者。it是Kotlin中let、run、apply、also等函数的lambda表达式中默认的参数名,用于表示被操作的对象。

data class Person(var name: String, var age: Int)

fun useWith() {
    val person = Person("Alice", 30)
    // 使用with函数来更新person对象的属性
    with(person) {
        this.name = "Bob" // 可以使用this,但通常可以省略
        age = 25 // 直接访问属性,因为with的作用域内this指向person
    }
    // 输出更新后的person对象
    println(person)  // 输出: Person(name=Bob, age=25)
}

对于上面的 helloReceiver 的实现,在Java文件中是这样的,

private final BroadcastReceiver helloReceiver = new BroadcastReceiver() {
	@Override
	public void onReceive(Context context, Intent intent) {
	}
}

在kotlin文件中,

val helloReceiver = object : BroadcastReceiver() {
	override fun onReceive(context: Context?, intent: Intent?) {
		if (intent == null) return
		when(intent.action) {
			"hello" -> {
                ...
            }
            "world" -> {
                ...
            }
       }
	}
}

上面的代码中, 使用object : BroadcastReceiver() {}来创建BroadcastReceiver的匿名实现。

7,字符串相关函数
isNullOrEmpty()用于检查字符串是否为空或null,toFloatOrNull()用于安全地将字符串转换为浮点数。

if (!volumeValue.isNullOrEmpty()) {
	val volume = volumeValue.toFloatOrNull() ?: return@runOnUiThread
}

?:是Elvis操作符,用于处理null情况。如果左侧的表达式的结果是null,则执行右侧的表达式。
return@是一个标签返回,在Kotlin中,你可以给lambda表达式或匿名函数加上标签,然后使用return@标签名来从该标签标记的代码块中返回,不再执行后面的代码。

8,枚举类型
在Java中,枚举类(enum)是一种特殊的类,它用于表示一组固定的常量,枚举类还可以包含字段、方法和构造函数。

public enum PlayMode {
	SHUFFLE,
	NORMAL,
	REPEAT
}

在 Kotlin 中,枚举类使用 enum class 关键字来定义,而不是 Java 中的 enum。枚举常量的定义方式与 Java 相同,直接列出它们即可。

enum class PlayMode {
	SHUFFLE,
	NORMAL,
	REPEAT
}

9,安全类型转换操作符
在Kotlin中,as? 是一个安全类型转换操作符,它尝试将一个对象转换为指定的类型,但如果转换失败(即目标类型与对象的实际类型不兼容),则不会抛出异常,而是返回 null。
这种安全类型转换操作符非常有用,因为它允许你在不担心类型转换异常的情况下处理可能为 null 的情况。在Kotlin中,处理 null 值是编程的一个重要方面,因为Kotlin的类型系统旨在减少 NullPointerException 的风险。

10,字符串插值功能kotlin中,字符串插值(StringInterpolation)是一项强大的功能,它允许你在字符串字面量中直接嵌入变量或表达式的值。这个功能通过美元符号( 在kotlin中,字符串插值(String Interpolation)是一项强大的功能,它允许你在字符串字面量中直接嵌入变量或表达式的值。这个功能通过美元符号()后跟变量名或表达式(用大括号{}包围复杂表达式)来实现。
变量插值:可以直接在字符串中使用$符号后跟变量名来插入变量的值

val name = "Kotlin"
val greeting = "Hello, $name!"  // 结果是 "Hello, Kotlin!"

对于更复杂的表达式,你可以使用${}语法。这允许你在字符串中插入任何有效的Kotlin表达式,并将其结果转换为字符串。

val a = 5
val b = 10
val result = "The sum of $a and $b is ${a + b}."  // 结果是 "The sum of 5 and 10 is 15."

函数调用:可以在字符串插值中调用函数,并将其返回值插入到字符串中。

fun checkFun():String = "AAA AA"
val d = "${checkFun()},BC"
println(d) //输出AAA AA,BC

11,JNI函数
在Java文件中,

public class JNIFile {
    static {
        System.loadLibrary("hello-control");
    }

    public static native int ctlIoctrl(int cmd, int helloIo);
    public static native int[] ctlRead();

    public static native FileDescriptor helloOpen(String path, int baudrate);
    public static native void helloClose(Object thiz);
    public static native String helloRead();
    public static native void helloWrite(byte data[], int size);

    public static native void sensInit();
    public static native int[] sensRead();
    public static native void sensClose();
    public static native void sensIoctl(int cmd, int arg);
}

在Kotlin文件中,

object JNIFile {
    init {
        // 加载本地库
        System.loadLibrary("hello-control")
    }

    @JvmStatic
    external fun ctlIoctrl(cmd: Int, helloIo: Int): Int

    @JvmStatic
    external fun ctlRead(): IntArray

    @JvmStatic
    external fun helloOpen(path: String, baudrate: Int): FileDescriptor

    @JvmStatic
    external fun helloClose(thiz: Any) // 注意:通常这个参数应该是与特定实例关联的某种类型的引用,但在这里我们保留为Any

    @JvmStatic
    external fun helloRead(): String

    @JvmStatic
    external fun helloWrite(data: ByteArray, size: Int)

    @JvmStatic
    external fun sensInit()

    @JvmStatic
    external fun sensRead(): IntArray

    @JvmStatic
    external fun sensClose()

    @JvmStatic
    external fun sensIoctl(cmd: Int, arg: Int)
}

上面的代码中,JNIFile是一个单例对象,它封装了一系列通过 JNI (Java Native Interface) 调用本地库(在这个例子中是 hello-control)的外部函数。
在 Kotlin 中,object 关键字用于定义一个单例对象。
在 Kotlin 中,external 关键字用于标记一个函数或属性为外部实现的,即它的实现不在 Kotlin 代码中,而是在其他地方,通常是在一个原生库(如 C 或 C++ 库)中。 这种机制允许 Kotlin 代码调用用其他语言编写的代码,这是通过 JNI(Java Native Interface)或其他类似的机制实现的。
external关键字用于声明一系列外部函数,这些函数在 Kotlin 代码中是不可见的(即没有函数体),但可以在运行时通过 JNI 调用到本地库中的实现。

12,init{}代码块
在Kotlin中,init {} 代码块用于初始化类的属性或执行一些在对象构造时需要立即运行的代码。
这个代码块在类的每个实例被创建时都会执行,而且是在构造函数体之前执行的(如果有构造函数的话)。init {} 代码块可以包含任意数量的语句,包括属性赋值、方法调用等。
关于init的关键点:
(1).属性初始化:你可以在 init {} 块中初始化类的属性,即使这些属性在声明时没有立即赋值。这对于依赖于其他属性值的属性特别有用,因为这些依赖关系可能在构造函数参数中还不清楚。
(2).执行顺序:init {} 块在构造函数之前执行。如果你定义了多个 init {} 块,它们将按照它们在类中出现的顺序执行。
(3).构造函数:如果类有主构造函数(即直接定义在类头上的构造函数),init {} 块将在主构造函数体之前执行。
对于次构造函数(通过 constructor 关键字定义的),每个次构造函数都需要直接或间接地调用主构造函数,因此 init {} 块仍然会在任何次构造函数执行之前运行。
(4).伴生对象:在伴生对象(companion object)中,init {} 块也可以用来初始化伴生对象的属性或执行一些初始化代码。这些代码块将在伴生对象的任何方法被调用之前执行。
(5).懒加载:虽然 init {} 块用于立即初始化,但如果你需要延迟初始化(即只在属性首次被访问时才初始化),你应该使用 by lazy 委托属性。

class Person(val name: String) {
    var age: Int
    var address: String

    init {
        // 在这里初始化age和address
        age = 0 // 假设默认年龄为0
        address = "Unknown" // 假设默认地址为"Unknown"
        println("Person $name is being initialized.")
    }

    // 构造函数之后的代码(如果有的话)...
}

fun main() {
    val person = Person("Alice")
    println("Person's name: ${person.name}, age: ${person.age}, address: ${person.address}")
}

上面的代码中,当创建 Person 类的实例时,init {} 块中的代码会首先执行,设置 age 和 address 的默认值,并打印一条消息。然后,构造函数体(如果有的话)会执行,但在这个例子中,构造函数体是空的,因为所有的初始化工作都在 init {} 块中完成了。

13,双精度浮点数的比较
在进行双精度值比较时,可以取适合的Double精度的比较值。

fun checkDoubleIsEqual(f1: Double, f2: Double): Boolean {
    // 例如,可以使用 1e-9(10的-9次方)作为 Double 类型的 epsilon。
    val doubleEpsilon: Double = 1e-9
    return Math.abs(f1 - f2) <= doubleEpsilon
}

14,类的定义
在Java文件中,

public class DbHelper extends SQLiteOpenHelper {
	public static final String DATABASE_NAME = "hello.db";
	public static final int DATABASE_VERSION = 1;
	public static final String TABLE_NAME = "world";
	public static final String COLUMN_ID = "id";
    public static final String COLUMN_NAME = "name";
	
	public DbHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, DATABASE_NAME, factory, DATABASE_VERSION);
    }
	
	@Override
    public void onCreate(SQLiteDatabase db) {
        Log.v(TAG, "onCreate");
        String sql = "CREATE TABLE " + TABLE_NAME + " ( " + COLUMN_ID + " INTEGER PRIMARY KEY ," + COLUMN_NAME + " TEXT )";
        db.execSQL(sql);
    }
	
	@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.v(TAG, "onUpgrade");
    }
}

在Kotlin文件中,

class DbHelper(context: Context?, name: String? = null, factory: SQLiteDatabase.CursorFactory? = null, version: Int) :
    SQLiteOpenHelper(context, DATABASE_NAME, factory, DATABASE_VERSION) {

    private val TAG = "DbHelper"

    companion object {
        const val DATABASE_NAME = "hello.db"
        const val DATABASE_VERSION = 1
        const val TABLE_NAME = "world"
        const val COLUMN_ID = "id"
        const val COLUMN_NAME = "name"
    }

    override fun onCreate(db: SQLiteDatabase) {
        Log.v(TAG, "onCreate")
        val sql = "CREATE TABLE $TABLE_NAME ( $COLUMN_ID INTEGER PRIMARY KEY, $COLUMN_NAME TEXT )"
        db.execSQL(sql)
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        Log.v(TAG, "onUpgrade")
        // 通常在这里添加升级数据库的逻辑,比如删除旧表、创建新表等
    }
}

Java的构造函数参数在Kotlin中可以直接写在类定义中,并且参数可以有默认值。
在上面的代码中,name和factory参数被设置为null的默认值,但实际上对于SQLiteOpenHelper来说,name和factory通常不需要提供,因此可以省略它们,或者将它们设为可选参数。
Kotlin中的类不需要public关键字,因为它是默认的。
使用companion object来定义静态常量,这是Kotlin中的常见做法。

15,init{}代码块
在Kotlin中,init {} 代码块用于初始化类的属性或执行一些在对象构造时需要立即运行的代码。
这个代码块在类的每个实例被创建时都会执行,而且是在构造函数体之前执行的(如果有构造函数的话)。init {} 代码块可以包含任意数量的语句,包括属性赋值、方法调用等。
关于init的关键点:
(1).属性初始化:你可以在 init {} 块中初始化类的属性,即使这些属性在声明时没有立即赋值。这对于依赖于其他属性值的属性特别有用,因为这些依赖关系可能在构造函数参数中还不清楚。
(2).执行顺序:init {} 块在构造函数之前执行。如果你定义了多个 init {} 块,它们将按照它们在类中出现的顺序执行。
(3).构造函数:如果类有主构造函数(即直接定义在类头上的构造函数),init {} 块将在主构造函数体之前执行。
对于次构造函数(通过 constructor 关键字定义的),每个次构造函数都需要直接或间接地调用主构造函数,因此 init {} 块仍然会在任何次构造函数执行之前运行。
(4).伴生对象:在伴生对象(companion object)中,init {} 块也可以用来初始化伴生对象的属性或执行一些初始化代码。这些代码块将在伴生对象的任何方法被调用之前执行。
(5).懒加载:虽然 init {} 块用于立即初始化,但如果你需要延迟初始化(即只在属性首次被访问时才初始化),你应该使用 by lazy 委托属性。

class Person(val name: String) {
    var age: Int
    var address: String

    init {
        // 在这里初始化age和address
        age = 0 // 假设默认年龄为0
        address = "Unknown" // 假设默认地址为"Unknown"
        println("Person $name is being initialized.")
    }

    // 构造函数之后的代码(如果有的话)...
}

fun main() {
    val person = Person("Alice")
    println("Person's name: ${person.name}, age: ${person.age}, address: ${person.address}")
}

上面的代码中,当创建 Person 类的实例时,init {} 块中的代码会首先执行,设置 age 和 address 的默认值,并打印一条消息。然后,构造函数体(如果有的话)会执行,但在这个例子中,构造函数体是空的,因为所有的初始化工作都在 init {} 代码块中完成了。