kotlin中object关键字的三种使用场景

前言

object是Kotlin中的一个重要的关键字,也是Java中没有的。object主要有以下三种使用场景:

  • 对象声明(Object Declaration)
  • 伴生对象(Companion Object)
  • 对象表达式(Object Expression)

下面就一一介绍它们所表示的含义、用法以及注意点,保证你在看完本篇之后就可以完全掌握object关键字的用法。

1. 对象声明(Object Declaration)

语法含义:将类的声明和定义该类的单例对象结合在一起(即通过object就实现了单例模式)

基本示例

object RepositoryManager{
 fun method(){
  println("I'm in object declaration")
 }
}

即将class关键字替换为object关键字,来声明一个类,与此同时也声明它的一个对象。只要编写这么多代码,这个类就已经是单例的了。

使用

a. 在Kotlin中:

fun main(args: Array<String>) {
 RepositoryManager.method()
}

像在Java中调用静态方法(在kotlin中没有静态方法)一样去调用其中定义的方法,但实际上是使用RepositoryManager类的单例对象去调用实例方法。如果对此还不能理解,可以看看下面对在Java中去使用的说明。

b. 在Java中:

public class JavaTest {
 public static void main(String[] args) {
  RepositoryManager.INSTANCE.method();
 }
}

换句话说,object declaration的类最终被编译成:一个类拥有一个静态成员来持有对自己的引用,并且这个静态成员的名称为INSTANCE,当然这个INSTANCE是单例的,故这里可以这么去使用。如果用Java代码来声明这个RepositoryManager的话,可以有如下代码:

class RepositoryManager{
 private RepositoryManager(){}
 public static final RepositoryManager INSTANCE = new RepositoryManager();

}

4) 注意点:

尽管和普通类的声明一样,可以包含属性、方法、初始化代码块以及可以继承其他类或者实现某个接口,但是它不能包含构造器(包括主构造器以及次级构造器)
它也可以定义在一个类的内部:

 class ObjectOuter {
  object Inner{
   fun method(){
    println("I'm in inner class")
   }
  }
 }
 fun main(args: Array<String>) {
  ObjectOuter.Inner.method()
 }

2、伴生对象(Companion object)

在阐述伴生对象之前,首先我们要明确一点:在Kotlin中是没有static关键字的,也就是意味着没有了静态方法和静态成员。那么在kotlin中如果要想表示这种概念,取而代之的是包级别函数(package-level function)和我们这里提到的伴生对象。至于它们之间的区别,不急,我们后面再说。

语法形式:

class A{
 companion object 伴生对象名(可以省略){
  //define method and field here
 }
}

基本示例:

class ObjectTest {

 companion object MyObjec{

  val a = 20

  fun method() {
   println("I'm in companion object")
  }
 }
}

使用:

a. 在Kotlin中:

fun main(args: Array<String>) {
 //方式一
 ObjectTest.MyObject.method()
 println(ObjectTest.MyObject.a)

 //方式二(推荐方式)
 ObjectTest.method()
 println(ObjectTest.a)
}

在这里请注意:在定义(定义时如果省略了伴生对象名,那么编译器会为其提供默认的名字Companion)和调用时伴生对象名是可以省略的。而且在方式二中,注意调用形式,是通过类名.方法名()的形式进行的,我们在没有生成ObjectTest类的对象时,调用了定义其内部伴生对象中定义的属性和方法,是不是类似Java中的静态方法的概念。

通过现象看本质

通过javap命令,让我们看看其生成的字节码:

注意红框中,这个MyObject成员变量的类型,是使用

MyObject这个内部类。注意它这里并没有外部类的引用,说明是以静态内部类的形式存在的。

还记得我们在前面遗留的问题:同样都可以用来替代Java中的static的概念,那么在伴生对象中定义的方法和包级别函数有什么区别呢?

先来反编译一个包含包级别函数的kt文件(或者说是类):

可以看出,一个名叫ObjectTest2.kt文件,实际上最终会生成一个名叫ObjectTest2Kt的类,而在这个kt文件中定义的顶级函数(包级别函数)是作为这个类的静态方法的形态存在的。

那么现在可以回答遗留的问题了:实际上就是平级类(姑且称之)中的静态方法和静态内部类中的方法的区别,因为静态内部类中的方法是可以访问外部类中定义的static方法和成员的,哪怕是private的(包括私有构造器,我们常用的基于静态内部类实现的单例模式就是基于这一点),而平级类中方法是访问不到当前类中静态的private成员的。如果你觉得文字这么描述还不够直观,那么我们来看下面这一张图(盗自Kotlin in action):

@JvmStatic注解:我们把前面定义的method方法上加上此注解,重新build工程,然后再来反编译ObjectTest和ObjectTest$MyObject这个两个类,看会有什么变化。

对于这个静态内部类而言,加与不加@JvmStatic注解其类的结构是没有变化的。但是对于目标类而言,很明显多了一个静态方法,这样我们就不难理解@JvmStatic注解的作用了:将伴生对象类中定义的实例方法和属性,添加到目标类中,并且以静态的形式存在。

对于伴生对象,最后再补充一点:一个类的伴生对象只能有一个。仔细想想也很好理解,伴生对象的名称是可以省略的。如果允许对应多个伴生对象,那么我们在多个伴生对象中都定义了一模一样的函数,在调用时到底是使用哪个伴生对象的方法呢?就会产生歧义,这样就不难理解这条语法规定了。

3、对象表达式(Object Expression)

Java的匿名内部类回顾:

在去学习对象表达式之前,我们先来回顾一下Java中的匿名内部类。

interface Contents {
 void absMethod();
}
public class Hello {

 public Contents contents() {
  return new Contents() {

   @Override
   public void absMethod() {
    System.out.println("method invoked...");
   }
  };
 }

 public static void main(String[] args) {

  Hello hello = new Hello();
  hello.contents().absMethod(); //打印method invoked...
 }
}
``` new Contents()

这个contents()方法返回的是一个匿名内部类的对象,这个匿名内部类实现了Contents接口。这些代码很熟悉,不多说了。现在提出两个局限性问题:

a. 如果在匿名内部类中新添加了一些属性和方法,那么在外界是无法调用的

return new Contents() {
private int i = 1;
  public int value() {
   return i;
  }

  @Override
  public void absMethod() {
   System.out.println("method invoked...");
  }
 };

public static void main(String[] args) {
 Hello hello = new Hello();
 hello.contents().absMethod();
 hello.contents().value(); //Cannot resolve method 'value()'
}

当你想使用这个value方法时,编译器会报错。也好理解,就是多态的知识,父类型的引用是无法知晓子类添加方法的存在的。

b. 一个匿名内部类肯定是实现了一个接口或者是继承一个类,并且只能是一个,用数学术语说是“有且只有一个”

2) 语法形式:

object [ : 接口1,接口2,类型1, 类型2]{}    //中括号中的可省略

3) 使用示例:

a. 实现一个接口或类

interface AA {
fun a()
}
fun main(args: Array) {
val aa = object : AA {
 override fun a() {
  println("a invoked")
 }
}

aa.a()
}

b. 不实现任何接口和类,并且在匿名内部类中添加方法

fun main(args: Array) {
val obj = object {
 fun a() {
  println("a invoked")
 }
}

obj.a() //打印:a invoked

}

从这个例子可以看出,前面我们提到的Java匿名内部类的第一个局限的地方在Kotlin中就不存在了,新添加的方法也是可以调用的

c. 实现多个接口和类

fun main(args: Array) {
val cc = object : AA, BB() {
override fun a() {
 }

 override fun b() {

 }

}

cc.a()
cc.b()
}

从这个例子可以看出,前面我们提到的Java匿名内部类的第二个局限性在kotlin中也不存在

4) 使用注意点:

这是Kotlin官方文档上的一段话:匿名对象只有定义成局部变量和private成员变量时,才能体现它的真实类型。如果你是将匿名对象作为public函数的返回值或者是public属性时,你只能将它看做是它的父类,当然你不指定任何类型时就当做Any看待。这时,你在匿名对象中添加的属性和方法是不能够被访问的。

再来举个例子帮助大家理解:

class MyTest {
private val foo = object {
 fun method() {
  println("private")
 }
}

val foo2 = object {
 fun method() {
  println("public")
 }
}

fun m() = object {
 fun method(){
  println("method")
 }
}

fun invoke(){

 val local = object {
  fun method(){
   println("local")
  }
 }

 local.method() //编译通过
 foo.method() //编译通过
 foo2.method() //编译通不过
 m().method() //编译通不过
}
}

5) 关于在匿名对象中访问同一作用下定义的局部变量的问题:

在Java中,如果在匿名内部类中访问外部定义的局部变量,那么该局部变量必须使用final关键字进行修饰,至于为什么大家可以看我之前的一篇博文。而在Kotlin中,这条限制没有了,看下面的例子:

var a = 1
val obj = object {
fun method() {
a++
}
}
obj.method()
println(a) //打印出2

再来解释一下:在Java中,实际上在method方法中使用的a实际上是局部变量a的一份拷贝,而不是它本身。而在Kotlin最终也是要编译成字节码供JVM去执行,所以本质上它是不会违背这一点的。那么它是怎么处理的呢?

当你访问的局部变量是val时,那么也是很Java一样,持有的是一份拷贝;而当你是一个可变变量(var)时,它的值是被存储在Ref这个类的实例成员中,Ref变量是final的,而他其中的成员变量是可以改变的。反编译后是可以看到Ref的身影的。

这里还有段有点意思的代码,给大家贴出:

fun tryToCountButtonClicks(button: Button): Int{
var clicks = 0

button.setOnClickListener{
 clicks++
}

return clicks
}

button是个按钮,这段代码的本意上是想要统计Button被点击的次数。但是这个函数的返回值始终是0,哪怕你点击再多次。因为你对局部变量clicks值得修改是异步的,而此函数的返回值是在执行时就确定了的,就是你的值还没有被修改,函数已经返回了。如果真的想统计点击次数,可以将clicks定义成类的成员变量。

4. 对比object declaration、Companion object以及object expression的初始化时机:

a. object declaration:当第一次访问它时才初始化,是一种懒初始化

b. Companion object:当它对应的类被加载后,它才初始化,类似Java中的静态代码块

c. object expression:一旦它被执行,立马初始化

至此,关于Kotlin中的object关键字的使用就介绍完了,希望大家能有所收获~

总结

到此这篇关于kotlin中object关键字的三种使用场景的文章就介绍到这了,更多相关kotlin object关键字使用场景内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Kotlin基础教程之dataclass,objectclass,use函数,类扩展,socket

    Kotlin基础教程之dataclass,objectclass,use函数,类扩展,socket Kotlin提供了一些机制来扩展已有的类,如下: 还记得我们之前写过的Point3D类吗?(将其略作修改,将成员变量改为Double类型) 让我们为其扩展一个length函数 扩展的方法很简单,只要在函数名前面加上类名就行了. 这样Point3D的对象就有了一个名为length的方法. 运行的结果不出所料: 除此之外,在Kotlin中还有一些特殊的类,比如Data Class: 有些类只包含数据,

  • kotlin中object关键字的三种使用场景

    前言 object是Kotlin中的一个重要的关键字,也是Java中没有的.object主要有以下三种使用场景: 对象声明(Object Declaration) 伴生对象(Companion Object) 对象表达式(Object Expression) 下面就一一介绍它们所表示的含义.用法以及注意点,保证你在看完本篇之后就可以完全掌握object关键字的用法. 1. 对象声明(Object Declaration) 语法含义:将类的声明和定义该类的单例对象结合在一起(即通过object就实

  • scala中停止循环的三种方式(推荐)

    Scala 循环 有的时候,我们可能需要多次执行同一块代码.一般情况下,语句是按顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推. 编程语言提供了更为复杂执行路径的多种控制结构. 循环语句允许我们多次执行一个语句或语句组,下面是大多数编程语言中循环语句的流程图: 开始正文介绍: 1:使用return关键字 object BreakLoop { //1.使用return关键字 def add():Unit= { for(i <- 1 to 10){ if(i==7){ //停止循环

  • 关于C++中定义比较函数的三种方法小结

    C++编程优与Pascal的原因之一是C++中存在STL(标准模板库).STL存在很多有用的方法. C++模板库中的许多方法都需要相关参数有序,例如Sort().显然,如果你想对一个集合进行排序,你必须要知道集合中的对象,那个在前那个在后.因此,学会如何定义比较方法是非常重要的. C++模板库的许多容器需要相关类型有序,例如set<T> 和priority_queue<T>. 这篇文章旨在告诉大家如何为一个类定义一个排序方法,以便在STL容器或者方法中使用. 作为一个C++程序员,

  • Java中break的第三种用法说明

    在Java中,break语句有三种用法,第一种是用于终止switch语句中的语句序列,第二种是用于退出循环,然而第三种是用作goto语句的"文明"形式! 我们知道,goto语句会破坏程序的结构,所以我们一般不会去使用这种功能.但是在某些情况下,goto语句对于流程控制很有价值并且结构合法. 比如当退出深度嵌套的一系列循环时,goto语句很好用.虽然Java中保留了goto关键字,但是没有goto语句,也没有任何用到这个关键字的地方,所以为了能够使用类似像goto语句这样的功能,Java

  • Java中线程Thread的三种方式和对比

    介绍 多线程主要的作用就是充分利用cpu的资源.单线程处理,在文件的加载的过程中,处理器就会一直处于空闲,但也被加入到总执行时间之内,串行执行切分总时间,等于每切分一个时间*切分后字符串的个数,执行程序,估计等几分钟能处理完就不错了.而多线程处理,文件加载与差分过程中 一.Java实现多线程的三种方式 1.继承Thread 通过Thread继承,并重写run方法来实现多线程,案例如下: public class ThreadPattern extends Thread { @Override p

  • Java项目开发中实现分页的三种方式总结

    目录 前言 使用 1.SpringDataJPA分页 2.MyBatis分页 3.Hutools工具类分页 总结 前言 Java项目开发中经常要用到分页功能,现在普遍使用SpringBoot进行快速开发,而数据层主要整合SpringDataJPA和MyBatis两种框架,这两种框架都提供了相应的分页工具,使用方式也很简单,可本人在工作中除此以外还用到第三种更方便灵活的分页方式,在这里一同分享给大家. 使用 主要分为SpringDataJPA分页.MyBatis分页.Hutools工具类分页几个部

  • oracle中if/else的三种实现方式详解

    1.标准sql规范 1.单个IF IF v=... THEN END IF; 2.IF ... ELSE IF v=... THEN ELSE t....; END IF; 3.多个IF IF v=... THEN ELSIF v=... THEN t...; END IFL 注意: 多个IF的是'ELSIF' 不是 ' ELSE IF' 2.decode函数 DECODE(VALUE,IF1,THEN1,IF2,THEN2,IF2,THEN2,..,ELSE) 表示如果value等于if1时,

  • C语言中函数指针的三种使用方法总结

     C语言中函数指针的三种使用方法总结 在这里分享一下自己的心得,希望和大家一起分享技术,如果有什么不足,还请大家指正.写出这篇目的,就是希望大家一起成长,我也相信技术之间没有高低,只有互补,只有分享,才能使彼此更加成长. 定义方式:int (*p)(int x, int y); 实现代码: #include <stdio.h> int sum(int x, int y){ return x + y; } int reduce(int x, int y){ return x - y; } int

  • Android中使用定时器的三种方法

    本文实例为大家分享了Android中使用定时器的三种方法,供大家参考,具体内容如下 图示: 因为都比较简单,所以就直接贴代码(虑去再次点击停止的操作),有个全局的Handler负责接收消息更新UI 第一种方法:Thread.sleep();方法 Runnable runnable = new Runnable() { @Override public void run() { while (true) { mHandler.sendEmptyMessage(0); try { Thread.sl

  • Java中实现线程的三种方式及对比_动力节点Java学院整理

    Java中创建线程主要有三种方式: 一.继承Thread类创建线程类 (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务.因此把run()方法称为执行体. (2)创建Thread子类的实例,即创建了线程对象. (3)调用线程对象的start()方法来启动该线程. package com.thread; public class FirstThreadTest extends Thread{ int i = 0; //重写run方法,run方法的方

随机推荐