【对比Java学Kotlin】扩展

2023-10-27

扩展概述

以 JDK 内置的集合 ArrayList 为例,如果我们想给其添加一个能力:交互两个元素 swap(index1, index2)。我们应该怎么做?常见的有如下方式:

  • 工具类+静态方法,以 ArrayList 和要交换的两个 index 为入参,操作 ArrayList 交换元素;
  • 继承 ArrayList 新写一个类,在类里面创建 swap() 方法,前提是基类是可以继承的,比如 java.lang.String 就不行;
  • 使用组合的方式,写一个 ArrayListWrapper,内置一个 ArrayList,对外提供 swap() 方法

其实,这几种方式都存在劣势。不管是工具类还是继承的方式还是装饰者模式的组合类,都是新建了一个类,很可能有同事以前已经创建了这个类了。但是如果你不知道新类的名字是啥,就很尴尬了。或者懒得问干脆自己新建一个,一个新的“轮子”诞生了。

如果我们在 AndroidStudio 里面输入 ArrayList 就能自动提示 swap 这个函数就好了,就跟原本就有这个方法一样。唉~~Kotlin 就提供了这种能力,这就是扩展。

扩展函数

继续以 swap() 方法为例。我们在 Kotlin 中使用如下格式完成,先定义 swap() 方法:

fun <T> MutableList<T>?.swap(i1: Int, i2: Int) {
    if (this == null) {
        return
    }
    val tmp = this[i1]
    this[i1] = this[i2]
    this[i2] = tmp
}

不失一般性的,如果我们想向某个已经存在的类 ReceiverClassType 中“添加”一个函数 func,那么我们可以写成:

ReceiverClassType.func()

如果允许接收空值,则可以写成:

ReceiverClassType?.func()

知道了定义格式,如何使用扩展函数呢?这样:

val list = mutableListOf(1, 2, 3)
list.swap(0, 2)

扩展函数在安卓开发中也有用武之地,比如我们要给原生控件 ImageView 扩展支持加载网络图片的功能,有了扩展之后我们可以这么做:

import android.widget.ImageView
import com.bumptech.glide.Glide

fun ImageView.loadImage(url: String) {
    Glide.with(this).load(url).into(this)
}

再比如,我们要给原生控件 TextView 添加富文本的能力,后端网络接口返回给前端一个 JSON 数组,数组里面的每个元素代表文本中的一段子串的样式:

fun TextView.setRichText(jsonArray: String) {
    val builder: SpannableStringBuilder = parse(jsonArray)
    setText(builder)
}

简单了解用法之后,一大串问题就来了:

  • 扩展函数的实现原理是什么?
  • 扩展函数放在什么位置呢?
  • 可以有可见性修饰符吗?
  • 可以作为类的成员方法吗?如果可以,支持重载、重写、多态吗?
  • 如果扩展函数与成员函数冲突了,谁的优先级更高呢?
  • 如何从 Java 代码里面调用扩展函数呢?

实现原理

继续以上面的 swap() 方法为例,我们看下其反编译出来的 Java 代码:

public final class SwapKt {
   public static final void swap(@Nullable List $this$swap, int index1, int index2) {
      if ($this$swap != null) {
         Object tmp = $this$swap.get(index1);
         $this$swap.set(index1, $this$swap.get(index2));
         $this$swap.set(index2, tmp);
      }
   }
}

其实就是一个工具类+static方法,并没有真的向目标类添加新的方法。
如果我们想在 Java 代码中引用扩展函数,我们需要使用 SwapKt.swap() 的形式:

import example.kotlin.pkg.SwapKt;
import java.util.ArrayList;

ArrayList<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
SwapKt.swap(list, 0 ,1);

放置位置

我们知道,kotlin 的普通函数是可以放置在单个文件里面(也就是 top-level),也可以作为成员函数放置在类体内,扩展函数也不例外。
我们能以 top-level 的形式放在单个文件 Swap.kt 中:

package example.kotlin.pkg

fun <T> MutableList<T>?.swap(index1: Int, index2: Int) {
    // ...
}

扩展函数也可以作为成员函数,即将类 A 的扩展函数在类 B 中定义,我们称 A 为接收类型(Receiver Type),称 B 为分发类型(Dispatch Type)。比如下面示例代码中的 Host 就是接收类型,而 Connection 是分发类型:

class Host(val hostname: String) {
    fun printHost() {
        print(hostname)
    }
}

class Connection(val host: Host, val port: Int) {
    fun printPort() {
        print(port)
    }

    fun Host.printConnectionString() {
        host.printHost()
        print(":")
        printPort()
    }

    fun connect() {
        host.printConnectionString()
    }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    //Host("kotl.in").printConnectionString()  // error, the extension function is unavailable outside Connection
}

既然在分发类型中定义接收类型的扩展方法,那在扩展方法中引用 this 究竟是谁呢?接收类型还是分发类型的?实际是接收类型的,如果想引用分发类型的 this,需要使用 this@DispatchType:

class Connection {
    fun Host.getConnectionString() {
        toString()         // calls Host.toString()
        this@Connection.toString()  // calls Connection.toString()
    }
}

既然扩展函数可以以成员函数的形式出现,那么就有成员函数的相应“待遇”:重写、重载、多态等,但这些“待遇”都是分发类型才有的,而接收类型是没有的,接收类型的函数都是静态引用,只跟接收类型有关,先来看个例子:

open class Shape
class Rectangle: Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) { // 只由 Shape 决定,而非其子类
    println(s.getName())
}

// 输出 Shape
printClassName(Rectangle())

// 输出 Shape
val shape: Shape = Rectangle()
printClassName(shape)

// 输出 Shape
val rect: Rectangle = Rectangle()
printClassName(rect as Shape)

在来看个更复杂的:

open class Shape
class Rectangle: Shape()

open class ShapeCaller {
    open fun Shape.getName() = "Shape in ShapeCaller"
    open fun Rectangle.getName() = "Rectangle in ShapeCaller"

    fun call(s: Shape) {
        println(s.getName())
    }
}

class RectangleCaller: ShapeCaller() {
    override fun Shape.getName() = "Shape in RectangleCaller"
    override fun Rectangle.getName() = "Rectangle in RectangleCaller"
}

fun main() {
    ShapeCaller().call(Shape())         // 输出 Shape in ShapeCaller
    ShapeCaller().call(Rectangle())     // 输出 Shape in ShapeCaller
    RectangleCaller().call(Shape())     // 输出 Shape in RectangleCaller
    RectangleCaller().call(Rectangle()) // 输出 Shape in RectangleCaller
}

由于 call(s: Shape) 里面的接收类型是 Shape,所有不能是 ShapeCaller 还是 RectangleCaller 都只会调用 Shape.getName(),但是调用父类还是子类里面的 Shape.getName() 却是符合多态的,具体决定于分发类型是 ShapeCaller 还是 RectangleCaller。

如果扩展函数和成员函数签名完全一致,那么成员函数优先级更高。

扩展属性

与扩展函数类似,Kotlin 还支持扩展属性。使用方法也类似,比如我们要给 MutableList 扩展一个本不存在的属性 lastIndex:

val <T> MutableList<T>?.lastIndex: Int
    get() {
        if (this == null) {
            return -1
        }

        return size - 1
    }

注意,因为扩展属性是本不存在的,因此其没有 Backing Field,我们也无法直接对其赋值,因此扩展属性只能是 val 类型的,否则会报错:

fun play() {
    val list = mutableListOf<String>()
    print(list.lastIndex)
    list.lastIndex = 1 // 报错
  
    // 结合扩展函数 swap() 实现交换首位元素
    list.swap(0, list.lastIndex)
}

最佳实践

在实际开发中,优先使用成员函数还是扩展函数呢?答案是优先使用成员函数,只有在无法使用成员函数,比如无法修改既有类或者虽然可以修改但是想控制使用权限的时候,再使用扩展函数。
同时,结合 infix 关键字我们可以实现很酷的功能,比如:

val ago = "ago"

infix fun Int.days(tense: String) = when (tense) {
    ago -> "2021-10-04 11:39:27"
    else -> "?"
}

fun main() {
//    val _2daysAgo = 2.days(ago)
    val _2daysAgo = 2 days ago
    println(_2daysAgo)
}

谁能想到 2 days ago 就是一行代码呢?而且是可以直接正常运行的代码,简直不要太酷!

更多常用扩展属性和方法见我的 Gist:Frequently used Kotlin extensions for Android

参考资料

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【对比Java学Kotlin】扩展 的相关文章

  • CookieManager.getInstance().removeAllCookie();不删除所有cookie

    我在应用程序的 onCreate 中调用 CookieManager getInstance removeAllCookie 我遇到了一个奇怪的问题 我看到 GET 请求中传递了意外的 cookie 值 事实上 cookie 值是一个非常非
  • 带有自定义阵列适配器的微调器不允许选择项目

    我使用自定义阵列适配器作为微调器 但是 当在下拉列表中选择一个项目时 下拉列表保留在那里 并且微调器不会更新 这是错误行为 与使用带有字符串的通用数组适配器相比 这是自定义类 我错过了什么吗 谢谢 public class Calendar
  • Clip 在 Java 中播放 WAV 文件时出现严重延迟

    我编写了一段代码来读取 WAV 文件 大小约为 80 mb 并播放该文件 问题是声音播放效果很差 极度滞后 你能告诉我有什么问题吗 这是我的代码 我称之为doPlayJframe 构造函数内的函数 private void doPlay f
  • ROOM迁移过程中如何处理索引信息

    CODE Entity tableName UserRepo indices Index value id unique true public class GitHubRepo PrimaryKey autoGenerate true p
  • 如何在C(Linux)中的while循环中准确地睡眠?

    在 C 代码 Linux 操作系统 中 我需要在 while 循环内准确地休眠 比如说 10000 微秒 1000 次 我尝试过usleep nanosleep select pselect和其他一些方法 但没有成功 一旦大约 50 次 它
  • Java中接口作为方法参数

    前几天去面试 被问到了这样的问题 问 反转链表 给出以下代码 public class ReverseList interface NodeList int getItem NodeList nextNode void reverse No
  • 如何将文件透明地传输到浏览器?

    受控环境 IE8 IIS 7 ColdFusion 当从 IE 发出指向媒体文件 例如 mp3 mpeg 等 的 GET 请求时 浏览器将启动关联的应用程序 Window Media Player 我猜测 IIS 提供文件的方式允许应用程序
  • Android相机意图:如何获取全尺寸照片?

    我正在使用意图来启动相机 Intent cameraIntent new Intent android provider MediaStore ACTION IMAGE CAPTURE getParent startActivityForR
  • 归并排序中的递归:两次递归调用

    private void mergesort int low int high line 1 if low lt high line 2 int middle low high 2 line 3 mergesort low middle l
  • 如何在 JFreeChart TimeSeries 图表上显示降雨指数和温度?

    目前 我的 TimeSeries 图表每 2 秒显示一个位置的温度 现在 如果我想每2秒显示一次降雨指数和温度 我该如何实现呢 这是我的代码 import testWeatherService TestWeatherTimeLapseSer
  • 检查 protobuf 消息 - 如何按名称获取字段值?

    我似乎无法找到一种方法来验证 protobuf 消息中字段的值 而无需显式调用其 getter 我看到周围的例子使用Descriptors FieldDescriptor实例到达消息映射内部 但它们要么基于迭代器 要么由字段号驱动 一旦我有
  • 如何创建像谷歌位置历史记录一样的Android时间轴视图?

    我想设计像谷歌位置历史这样的用户界面 我必须为我正在使用的应用程序复制此 UIRecyclerView 每行都是水平的LinearLayout其中包含右侧的图标 线条和视图 该线是一个FrameLayout具有圆形背景和半透明圆圈Views
  • 将 Long 转换为 DateTime 从 C# 日期到 Java 日期

    我一直尝试用Java读取二进制文件 而二进制文件是用C 编写的 其中一些数据包含日期时间数据 当 DateTime 数据写入文件 以二进制形式 时 它使用DateTime ToBinary on C 为了读取 DateTime 数据 它将首
  • 使用 AWS Java SDK 为现有 S3 对象设置 Expires 标头

    我正在更新 Amazon S3 存储桶中的现有对象以设置一些元数据 我想设置 HTTPExpires每个对象的标头以更好地处理 HTTP 1 0 客户端 我们正在使用AWS Java SDK http aws amazon com sdkf
  • Java中未绑定通配符泛型的用途和要点是什么?

    我不明白未绑定通配符泛型有什么用 具有上限的绑定通配符泛型 stuff for Object item stuff System out println item Since PrintStream println 可以处理所有引用类型 通
  • 应用程序关闭时的倒计时问题

    我制作了一个 CountDownTimer 代码 我希望 CountDownTimer 在完成时重新启动 即使应用程序已关闭 但它仅在应用程序正在运行或重新启动应用程序时重新启动 因此 如果我在倒计时为 00 10 分钟 秒 时关闭应用程序
  • 保护 APK 中的字符串

    我正在使用 Xamarin 的 Mono for Android 开发一个 Android 应用程序 我目前正在努力使用 Google Play API 添加应用内购买功能 为此 我需要从我的应用程序内向 Google 发送公共许可证密钥
  • Android 如何聚焦当前位置

    您好 我有一个 Android 应用程序 可以在谷歌地图上找到您的位置 但是当我启动该应用程序时 它从非洲开始 而不是在我当前的城市 国家 位置等 我已经在developer android com上检查了信息与位置问题有关 但问题仍然存在
  • 在webview android中加载本地html文件

    我正在尝试在 android 的 webview 中加载 html 文件的内容 但是 它给了我 网页不可用错误 如果我尝试使用谷歌或雅虎等网站 它们就会起作用 html文件位于src gt main gt assests gt index
  • 找到 Android 浏览器中使用的 webkit 版本?

    有没有办法知道某些特定手机上的 Android 浏览器使用的是哪个版本的 webkit 软件 如果有一个您可以浏览以获取该信息的 URL 那就太好了 但任何其他方式也很好 如果你知道 webkit 版本 你就知道 html5 支持多少 至少

随机推荐

  • unity的常见错误处理

    黄色提示 文件丢失 第一种情况 显示文件丢失 就去把文件拖到丢失的目录下 第二种情况 代码错误 无法识别 一般是调用的参数打错 红色提示 无法识别 当unity出现下列代码的情况时 we can t assign a new guid be
  • rabbitmq-给消息设置过期时间(九)

    TTL 全称 Time To Live 存活时间 过期时间 当消息到达存活时间后 还没有被消费 会被自动清除 RabbitMQ可以对消息设置过期时间 也可以对整个队列 Queue 设置过期时间 方式一 通过给队列配置属性设置消息的过期时间
  • js(art-­template、cookie、session、分页、注册、form、audio、video、移动端event、touch.js)

    art template 1 介绍 art template是一个简单且超快速的模板引擎 可通过范围内预先声明的技术优化模板渲染速度 从而实现接近JavaScript极限的运行时性能 同时它支持nodeJS和浏览器 2 基本使用 引入art
  • k8s之PV、PVC和StorageClass

    PV 什么是PV PV 描述的 则是一个具体的 Volume 的属性 比如 Volume 的类型 挂载目录 远程存储服务器地址等 创建PV 使用yaml来定义PV apiVersion v1 kind PersistentVolume me
  • Java多线程简析——Synchronized(同步锁)、Lock以及线程池

    Java多线程 Java中 可运行的程序都是有一个或多个进程组成 进程则是由多个线程组成的 最简单的一个进程 会包括mian线程以及GC线程 线程的状态 线程状态由以下一张网上图片来说明 在图中 红框标识的部分方法 可以认为已过时 不再使用
  • 关于npm cnpm yarm pnmp

    npm npm是什么 npm是node package manager的简称 node js 的包管理器 用于node插件管理 包括安装 卸载 管理依赖等 pm 是随同 node js 一起安装的包管理工具 能解决 node js 代码部署
  • mysql建表注意事项。

    1 表达是否概念的字段 必须使用is xxx的方式 数据类型是unsigned tinyint 1表示是 0表示否 pojo类中任何布尔类型的变量 都不要加is前缀 2 表名 字段名必须使用小写字母或者数字 禁止出现数字开头 禁止只出现两个
  • ShardingSphere系列二(ShardingSphere实现分库分表)

    文章目录 1 高性能数据库集群架构模式 1 1 读写分离 1 2 数据分片 分库分表 2 实现方式 2 1 程序代码封装 2 2 中间件封装 2 3 常用解决方案 3 ShardingSphere 4 ShardingJDBC 4 1 核心
  • 项目简单日志配置(控制台和输出文件)

    首先引入相关的log4j的jar包 然后配置log4j properties文件 简单配置如下 日志的输出级别以及输出目的地 这里是配置输出到控制台与日志文件 log4j常用的优先级FATAL gt ERROR gt WARN gt INF
  • MATLAB----模糊聚类

    data 1739 94 1675 15 2395 96 3 373 3 3087 05 2429 47 4 1756 77 1652 1514 98 3 864 45 1647 31 2665 9 1 222 85 3059 54 200
  • ReactHooks常用钩子总结

    ReactHooks常用钩子 重学react 这里对常用的ReactHooks做一下简单的总结 1 useState useState 会返回一个数组 一个 state 一个更新 state 的函数 在初始化渲染期间 返回的状态 state
  • 左值与右值

    目录 一 类型 二 地址 三 举例 一 类型 1 均与类型无关 int型 float型等等 均有 二 地址 1 右值不可取地址 引用的右值 程序没有分配内存区域 无地址 2 左值可以取地址 地址为所引用的实例 变量 对象的地址 int ma
  • Qt利用setStyleSheet设置样式

    Qt中设置按钮或QWidget的外观是 可以使用QT Style Sheets来进行设置 非常方便 可以用setStyleSheet font bold font size 20px color rgb 241 70 62 backgrou
  • [总结]怎么给VM(虚拟机)添加新磁盘

    1 关闭系统 点击VM gt Settings弹出的Add Hardware Wizard对话框 点击Add gt Hard Disk gt Next gt Create a new virtual disk gt SCSI Recomme
  • 软件设计 基础篇(二) 交互设计

    软件设计 基础篇 系列文章目录 软件设计 基础篇 二 交互设计 文章目录 软件设计 基础篇 系列文章目录 1 软件设计 1 1 设计简介 1 2 设计基础 2 交互设计 2 1 交互概述 2 2 交互起源 总结 1 软件设计 软件设计是从软
  • Shell函数调用

    文章目录 一 函数基本格式 二 函数调用 2 1函数中调用 2 2函数调用函数 2 3外部调用 2 4案例 三 总结 在shell脚本中 有些命令或者某些操作需要频繁的使用时 每次都重新写太过繁琐 这时我们就可以使用函数 当需要使用时 直接
  • 2022年开始学习Delphi并成为Delphi程序员的5个重要原因

    Delphi 是编程界的传奇语言之一 是软件开发历史的基石 随着新平台和框架的出现 新语言脱颖而出 但 Delphi 因其作为跨平台本机原生开发工具的可靠性和有效性而在IT界发展趋势来来去去的大潮中站稳了脚跟 更重要的是 随着时间的推移 D
  • 用python做一个随机点名程序(不重复点名)

    用python做一个简单的随机点名程序 不重复点名 这是我来到csdn的第一篇文章 内容如果有瑕疵的地方或者代码可以进一步改善 请大家对我指点一二 谢谢 废话不多说 上代码 import random 导入随机模块 import pytts
  • centos7 新磁盘挂载扩展到根目录

    摘要 局域网 ESXi 上搭建的 gitlab 代码仓库不能使用 查看了一下是 dev mapper centos root 路径存储满了 这里记录一下把新增磁盘挂载到根目录下的过程 在为 liunx 虚拟机扩充磁盘的时候 只能是在关机状态
  • 【对比Java学Kotlin】扩展

    扩展概述 以 JDK 内置的集合 ArrayList 为例 如果我们想给其添加一个能力 交互两个元素 swap index1 index2 我们应该怎么做 常见的有如下方式 工具类 静态方法 以 ArrayList 和要交换的两个 inde