百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

Kotlin与Java的不同之处(java和kotlin哪个更值得学)

liuian 2025-03-25 15:18 21 浏览

伴生对象

在 Kotlin 中并不没有 static 这个关键字,该如何处理呢?这里需要用到 Kotlin 的伴生对象来处理。

类内部的对象声明可以用 companion 关键字标记:

该伴生对象的成员可通过只使用类名作为限定符来调用:

可以省略伴生对象的名称,在这种情况下将使用名称 Companion:

伴生对象的作用

类似于 Java 中使用类访问静态成员的语法。因为 Kotlin 取消了 static 关键字,所以 Kotlin 引入伴生对象来弥补没有静态成员的不足。可见,伴生对象的主要作用就是为其所在的外部类模拟静态成员。

在 Java 代码中调用伴生对象

如何在 Java 代码中调用 Kotlin 的伴生对象呢?

  • 如果声明伴生对象有名称,则使用:
  • 如果声明伴生对象无名称,则采用 Companion 关键字调用:

@JvmField 和 @JvmStatic 的使用

在上面的例子中,我们知道了可以在 Java 代码中调用 Kotlin 中伴生对象的成员,类似于 Java 类中的静态成员。但是看上去和 Java 中的还是略有区别,因为类名和方法名/属性setter,getter方法名之间多了个伴生对象的名称或者 Companion 关键字。如何使其在调用的时候与 Java 中的调用看上去一样呢?

Kotlin 为我们提供了 @JvmField 和 @JvmStatic 两个注解。@JvmField 使用在属性上,@JvmStatic 使用在方法上。如:

这样我们在 Java 代码中调用的时候就和 Java 类调用静态成员的形式一致了,Kotlin 代码调用方式不变:

const 关键字

在伴生对象中,我们可能需要声明一个常量,目的是等同于 Java 中的静态常量。有两种方式,一种是上面所提到的使用 @JvmField 注解,另一种则是使用 const 关键字修饰。这两种声明方式都等同于 Java 中 static final 所修饰的变量。如下代码:

扩展属性和扩展方法

扩展函数

Kotlin的扩展函数可以让你作为一个类成员进行调用的函数,但是是定义在这个类的外部。这样可以很方便的扩展一个已经存在的类,为它添加额外的方法

下面我们为String添加一个toInt的方法

在这个扩展函数中,你可以直接访问你扩展的类的函数和属性,就像定义在这个类中的方法一样,但是扩展函数并不允许你打破封装。跟定义在类中方法不同,它不能访问那些私有的、受保护的方法和属性。

扩展函数的导入

我们直接在包里定义扩展函数。这样我们就可以在整个包里面使用这些扩展,如果我们要使用其他包的扩展,我们就需要导入它。导入扩展函数跟导入类是一样的方式。

有时候,可能你引入的第三方包都对同一个类型进行了相同函数名扩展,为了解决冲突问题,你可以使用下面的方式对扩展函数进行改名

扩展函数不可覆盖

扩展方法的原理

Kotlin 中类的扩展方法并不是在原类的内部进行拓展,通过反编译为Java代码,可以发现,其原理是使用装饰模式,对源类实例的操作和包装,其实际相当于我们在 Java中定义的工具类方法,并且该工具类方法是使用调用者为第一个参数的,然后在工具方法中操作该调用者

如:

反编译为对应的Java代码:

扩展属性

类的扩展属性原理其实与扩展方法是一样的,只是定义的形式不同,扩展属性必须定义get和set方法

为MutableList扩展一个firstElement属性:

反编译后的java代码如下:

内部类

kotlin的内部类与java的内部类有点不同java的内部类可以直接访问外部类的成员,kotlin的内部类不能直接访问外部类的成员,必须用inner标记之后才能访问外部类的成员

  • 没有使用inner标记的内部类

反编译后的java代码

  • 用inner标记的内部类

反编译后的java代码

从上面可以看出,没有使用inner标记的内部类最后生成的是静态内部类,而使用inner标记的生成的是非静态内部类

匿名内部类

匿名内部类主要是针对那些获取抽象类或者接口对象而来的。最常见的匿名内部类View点击事件:

上面这个是java匿名内部类的写法,kotlin没有new关键字,那么kotlin的匿名内部类该怎么写呢?

方法的参数是一个匿名内部类,先写object:,然后写你的参数类型View.OnClickListener{}

kotlin还有一个写法lambda 表达式,非常之方便:

数据类

在Java中没有专门的数据类,常常是通过JavaBean来作为数据类,但在Kotlin中提供了专门的数据类。

  • Java

从上面的例子中可以看到,如果要使用数据类,需要手动写相应的setter/getter方法(尽管IDE也可以批量生成),但是从代码阅读的角度来说,在属性较多的情况下,诸多的seeter/getter方法还是不利于代码的阅读和维护。

  • Kotlin
    在Kotlin中,可以通过关键字data来生成数据类:

即在class关键字之前添加data关键字即可。编译器会根据主构造函数中的参数生成相应的数据类。自动生成setter/getter、toString、hashCode等方法

要声明一个数据类,需要满足:

  • 主构造函数中至少有一个参数
  • 主构造函数中所有参数需要标记为val或var
  • 数据类不能是抽象、开发、密封和内部的

枚举类

枚举类是一种特殊的类,kotlin可以通过enum class关键字定义枚举类。

枚举类可以实现0~N个接口;

  • 枚举类默认继承于kotlin.Enum类(其他类最终父类都是Any),因此kotlin枚举类不能继承类;
  • 非抽象枚举类不能用open修饰符修饰,因此非抽象枚举类不能派生子类;
  • 抽象枚举类不能使用abstract关键字修饰enum class,抽象方法和抽象属性需要使用;
  • 枚举类构造器只能使用private修饰符修饰,若不指定,则默认为private;
  • 枚举类所有实例在第一行显式列出,每个实例之间用逗号隔开,整个声明以分号结尾;
  • 枚举类是特殊的类,也可以定义属性、方法、构造器;
  • 枚举类应该设置成不可变类,即属性值不允许改变,这样更安全;
  • 枚举属性设置成只读属性后,最好在构造器中为枚举类指定初始值,如果在声明时为枚举指定初始值,会导致所有枚举值(或者说枚举对象)的该属性都一样。

定义枚举类

枚举类实现接口

  1. 枚举值分别实现接口的抽象成员
  1. 枚举类统一实现接口的抽象成员
  1. 分别实现抽象枚举类抽象成员

委托

委托模式 是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承。

Java中委托:

Kotlin:

by表示 p 将会在 PrintImpl 中内部存储, 并且编译器将自动生成转发给 p 的所有 Printer 的方法。

委托属性

有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好。例如包括:

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

为了涵盖这些(以及其他)情况,Kotlin 支持 委托属性 。

委托属性的语法是:

在 by 后面的表达式是该 委托, 因为属性对应的 get()(和 set())会被委托给它的 getValue() 和 setValue() 方法。

标准委托:

Kotlin 标准库为几种有用的委托提供了工厂方法。

  1. 延迟属性 Lazy
    lazy() 接受一个 lambda 并返回一个 Lazy实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。例如:
  1. 可观察属性 Observable
    Delegates.observable() 接受两个参数:初始值和修改时处理程序(handler)。每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值和新值:

如果想拦截赋的新值,并根据你是不是想要这个值来决定是否给属性赋新值,可以使用 vetoable() 取代 observable(),接收的参数和 observable 一样,不过处理程序 返回值是 Boolean 来决定是否采用新值,即在属性被赋新值生效之前 会调用传递给 vetoable 的处理程序。例如:

  1. 把属性存在map 中
    一个常见的用例是在一个映射(map)里存储属性的值。这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

例如:

在上例中,委托属性会从构造函数传入的map中取值(通过字符串键——属性的名称),如果遇到声明的属性名在map 中找不到对应的key 名,或者key 对应的value 值的类型与声明的属性的类型不一致,会抛出异常。

内联函数

当一个函数被声明为inline时,它的函数体是内联的,也就是说,函数体会被直接替换到函数被调用地方

inline函数(内联函数)从概念上讲是编译器使用函数实现的真实代码来替换每一次的函数调用,带来的最直接的好处就是节省了函数调用的开销,而缺点就是增加了所生成字节码的尺寸。基于此,在代码量不是很大的情况下,我们是否有必要将所有的函数定义为内联?让我们分两种情况进行说明:

  1. 将普通函数定义为内联:众所周知,JVM内部已经实现了内联优化,它会在任何可以通过内联来提升性能的地方将函数调用内联化,并且相对于手动将普通函数定义为内联,通过JVM内联优化所生成的字节码,每个函数的实现只会出现一次,这样在保证减少运行时开销的同时,也没有增加字节码的尺寸;所以我们可以得出结论,对于普通函数,我们没有必要将其声明为内联函数,而是交给JVM自行优化。
  2. 将带有lambda参数的函数定义为内联:是的,这种情况下确实可以提高性能;但在使用的过程中,我们会发现它是有诸多限制的,让我们从下面的例子开始展开说明:

假如我们这样调用doSomething:

上面的调用会被编译成:

从上面编译的结果可以看出,无论doSomething函数还是action参数都被内联了,很棒,那让我们换一种调用方式:

上面的调用会被编译成:

doSomething函数被内联,而action参数没有被内联,这是因为以函数型变量的形式传递给doSomething的lambda在函数的调用点是不可用的,只有等到doSomething被内联后,该lambda才可以正常使用。

通过上面的例子,我们对lambda表达式何时被内联做一下简单的总结:

  1. 当lambda表达式以参数的形式直接传递给内联函数,那么lambda表达式的代码会被直接替换到最终生成的代码中。
  2. 当lambda表达式在某个地方被保存起来,然后以变量形式传递给内联函数,那么此时的lambda表达式的代码将不会被内联。

上面对lambda的内联时机进行了讨论,消化片刻后让我们再看最后一个例子:

上面的例子是否有问题?是的,编译器会抛出“Illegal usage of inline-parameter”的错误,这是因为Kotlin规定内联函数中的lambda参数只能被直接调用或者传递给另外一个内联函数,除此之外不能作为他用;那我们如果确实想要将某一个lambda传递给一个非内联函数怎么办?我们只需将上述代码这样改造即可:

很简单,在不需要内联的lambda参数前加上noinline修饰符就可以了。

以上便是我对内联函数的全部理解,通过掌握该特性的运行机制,相信大家可以做到在正确的时机使用该特性,而非滥用或因恐惧弃而不用。

Kotlin下单例模式

饿汉式实现

懒汉式

上述代码中,我们可以发现在Kotlin实现中,我们让其主构造函数私有化并自定义了其属性访问器,其余内容大同小异。

  • 如果有小伙伴不清楚Kotlin构造函数的使用方式。请点击 - - - 构造函数
  • 不清楚Kotlin的属性与访问器,请点击 - - -属性和字段

线程安全的懒汉式

大家都知道在使用懒汉式会出现线程安全的问题,需要使用使用同步锁,在Kotlin中,如果你需要将方法声明为同步,需要添加@Synchronized注解。

双重校验锁式

哇!小伙伴们惊喜不,感不感动啊。我们居然几行代码就实现了多行的Java代码。其中我们运用到了Kotlin的延迟属性 Lazy。

Lazy内部实现

观察上述代码,因为我们传入的mode =
LazyThreadSafetyMode.SYNCHRONIZED,

那么会直接走 SynchronizedLazyImpl,我们继续观察SynchronizedLazyImpl。

Lazy接口

SynchronizedLazyImpl实现了Lazy接口,Lazy具体接口如下:

继续查看SynchronizedLazyImpl,具体实现如下:

SynchronizedLazyImpl内部实现

通过上述代码,我们发现 SynchronizedLazyImpl 覆盖了Lazy接口的value属性,并且重新了其属性访问器。其具体逻辑与Java的双重检验是类似的。

到里这里其实大家还是肯定有疑问,我这里只是实例化了SynchronizedLazyImpl对象,并没有进行值的获取,它是怎么拿到高阶函数的返回值呢?。这里又涉及到了委托属性

委托属性语法是:val/var <属性名>: <类型> by <表达式>。在 by 后面的表达式是该 委托, 因为属性对应的 get()(和 set())会被委托给它的 getValue() 和 setValue() 方法。属性的委托不必实现任何的接口,但是需要提供一个 getValue() 函数(和 setValue()——对于 var 属性)。

而Lazy.kt文件中,声明了Lazy接口的getValue扩展函数。故在最终赋值的时候会调用该方法。

静态内部类式

静态内部类的实现方式,也没有什么好说的。Kotlin与Java实现基本雷同。

补充

在该篇文章结束后,有很多小伙伴咨询,如何在Kotlin版的Double Check,给单例添加一个属性,这里我给大家提供了一个实现的方式。(不好意思,最近才抽出时间来解决这个问题)

其中关于?:操作符,如果 ?: 左侧表达式非空,就返回其左侧表达式,否则返回右侧表达式。请注意,当且仅当左侧为空时,才会对右侧表达式求值。

Kotlin 智能类型转换

对于子父类之间的类型转换

  • 先看这样一段 Java 代码
  • 尽管在 main 函数中,对 person 这个对象进行了类型判断,但是在使用的时候还是需要强制转换成 Student 类型,这样是不是很不智能?
  • 同样的情况在 Kotlin 中就变得简单多了
  • 在 Kotlin 中,只要对类型进行了判断,就可以直接通过父类的对象去调用子类的函数了

安全的类型转换

  • 还是上面的那个例子,如果我们没有进行类型判断,并且直接进行强转,会怎么样呢?
  • 结果就只能是 Exception in thread "main" java.lang.ClassCastException
  • 那么在 Kotlin 中是不是会有更好的解决方法呢?
  • 在转换操作符后面添加一个 ?,就不会把程序 crash 掉了,当转化失败的时候,就会返回一个 null

在空类型中的智能转换

  • 需要提前了解 Kotlin 类型安全的相关知识(Kotlin 中的类型安全(对空指针的优化处理))
  • aString 在定义的时候定义成了有可能为 null,按照之前的写法,我们需要这样写
  • 但是已经进行了是否为 String 类型的判断,所以就一定 不是 空类型了,也就可以直接输出它的长度了

T.()->Unit 、 ()->Unit

在做kotlin开发中,经常看到一些系统函数里,用函数作为参数

.()-Unit与()->Unit的区别是我们调用时,在代码块里面写this,的时候,两个this代表的含义不一样,T.()->Unit里的this代表的是自身实例,而()->Unit里,this代表的是外部类的实例

相关推荐

教你把多个视频合并成一个视频的方法

一.情况介绍当你有一个m3u8文件和一个目录,目录中有连续的视频片段,这些片段可以连成一段完整的视频。m3u8文件打开后像这样:m3u8文件,可以理解为播放列表,里面是播放视频片段的顺序。视频片段像这...

零代码编程:用kimichat合并一个文件夹下的多个文件

一个文件夹里面有很多个srt字幕文件,如何借助kimichat来自动批量合并呢?在kimichat对话框中输入提示词:你是一个Python编程专家,完成如下的编程任务:这个文件夹:D:\downloa...

Java APT_java APT 生成代码

JavaAPT(AnnotationProcessingTool)是一种在Java编译阶段处理注解的工具。APT会在编译阶段扫描源代码中的注解,并根据这些注解生成代码、资源文件或其他输出,...

Unit Runtime:一键运行 AI 生成的代码,或许将成为你的复制 + 粘贴神器

在我们构建了UnitMesh架构之后,以及对应的demo之后,便着手于实现UnitMesh架构。于是,我们就继续开始UnitRuntime,以用于直接运行AI生成的代码。PS:...

挣脱臃肿的枷锁:为什么说Vert.x是Java开发者手中的一柄利剑?

如果你是一名Java开发者,那么你的职业生涯几乎无法避开Spring。它如同一位德高望重的老国王,统治着企业级应用开发的大片疆土。SpringBoot的约定大于配置、SpringCloud的微服务...

五年后,谷歌还在全力以赴发展 Kotlin

作者|FredericLardinois译者|Sambodhi策划|Tina自2017年谷歌I/O全球开发者大会上,谷歌首次宣布将Kotlin(JetBrains开发的Ja...

kotlin和java开发哪个好,优缺点对比

Kotlin和Java都是常见的编程语言,它们有各自的优缺点。Kotlin的优点:简洁:Kotlin程序相对于Java程序更简洁,可以减少代码量。安全:Kotlin在类型系统和空值安全...

移动端架构模式全景解析:从MVC到MVVM,如何选择最佳设计方案?

掌握不同架构模式的精髓,是构建可维护、可测试且高效移动应用的关键。在移动应用开发中,选择合适的软件架构模式对项目的可维护性、可测试性和团队协作效率至关重要。随着应用复杂度的增加,一个良好的架构能够帮助...

颜值非常高的XShell替代工具Termora,不一样的使用体验!

Termora是一款面向开发者和运维人员的跨平台SSH终端与文件管理工具,支持Windows、macOS及Linux系统,通过一体化界面简化远程服务器管理流程。其核心定位是解决多平台环境下远程连接、文...

预处理的底层原理和预处理编译运行异常的解决方案

若文章对您有帮助,欢迎关注程序员小迷。助您在编程路上越走越好![Mac-10.7.1LionIntel-based]Q:预处理到底干了什么事情?A:预处理,顾名思义,预先做的处理。源代码中...

为“架构”再建个模:如何用代码描述软件架构?

在架构治理平台ArchGuard中,为了实现对架构的治理,我们需要代码+模型描述所要处理的内容和数据。所以,在ArchGuard中,我们有了代码的模型、依赖的模型、变更的模型等,剩下的两个...

深度解析:Google Gemma 3n —— 移动优先的轻量多模态大模型

2025年6月,Google正式发布了Gemma3n,这是一款能够在2GB内存环境下运行的轻量级多模态大模型。它延续了Gemma家族的开源基因,同时在架构设计上大幅优化,目标是让...

比分网开发技术栈与功能详解_比分网有哪些

一、核心功能模块一个基本的比分网通常包含以下模块:首页/总览实时比分看板:滚动展示所有正在进行的比赛,包含比分、比赛时间、红黄牌等关键信息。热门赛事/焦点战:突出显示重要的、关注度高的比赛。赛事导航...

设计模式之-生成器_一键生成设计

一、【概念定义】——“分步构建复杂对象,隐藏创建细节”生成器模式(BuilderPattern):一种“分步构建型”创建型设计模式,它将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建...

构建第一个 Kotlin Android 应用_kotlin简介

第一步:安装AndroidStudio(推荐IDE)AndroidStudio是官方推荐的Android开发集成开发环境(IDE),内置对Kotlin的完整支持。1.下载And...