Kotlin入门(12)类的概貌与构造

上一篇文章提到泛型函数appendString是在类外面定义,这不免使人疑惑,类里面又该怎样定义成员函数呢?为解答这个疑问,接下来的几篇文章将好好描述一下Kotlin如何操作类及其对象,本篇文章先对类的定义进行说明并加以运用。

之前我们已经多次见过的类MainActivity,在Java代码中该类的写法如下所示:

public class MainActivity extends AppCompatActivity {
}

而对应的Kotlin代码是下面这样的:

class MainActivity : AppCompatActivity() {
}

根据上述代码简单地比较,Kotlin对类的写法与Java之间有以下几点区别:
1、Kotlin省略了关键字public;
2、Kotlin用冒号“:”代替extends,也就是通过冒号表示继承关系;
3、Kotlin进行继承时,父类后面多了括号“()”;
表面上二者区别不大,其实类这部分大有玄机,真正用Kotlin实现的话让人出乎意料,接下来且待笔者细细道来。

从最简单的类定义开始,下面是名为Animal的动物类定义代码:

class Animal {
    //类的初始化函数
    init {
        //Kotlin使用println替换Java的System.out.println
        println("Animal:这是个动物的类")
    }
}

对应为Animal类创建实例的代码如下:

    btn_class_simple.setOnClickListener {
        //var animal: Animal = Animal()
        //因为根据等号后面的构造函数已经明确知道这是个Animal的实例
        //所以声明对象时可以不用指定它的类型
        var animal = Animal()
        tv_class_init.text = "简单类的初始化结果见日志"
    }

然后继续给Kotlin找茬,不费多少功夫又发现了它跟Java的三点不同之处:
1、Kotlin初始化函数(看似构造函数?)的名字叫init,不像Java那样把类名作为构造函数的名称;
2、Kotlin打印日志使用了类似C语言的println方法,而非Java的System.out.println;
3、Kotlin创建实例时省略了关键字new;

既然Kotlin把init当作初始化函数,那么是否意味着,构造函数的参数应该添加在init名称后面?可事情往往不是你想的那样,Kotlin作为新时代的编程语言,它的设计总是突破常规。前面介绍函数的时候,提到Kotlin把函数看成是一种特殊的变量,至于类某种意义上算是一种特殊的函数。所以构造函数的输入参数得直接加到类名后面,而init函数仅仅表示创建类实例之时的初始化动作,下面是添加了入参的类定义代码:

//如果主构造函数没有注解说明,则类名后面的constructor可以省略
//class AnimalMain (context:Context, name:String) {
class AnimalMain constructor(context:Context, name:String) {
    init {
        context.toast("这是只$name");
    }
}

然而以上代码似乎存在着问题,因为一个类可能会有多个构造函数,像自定义视图常常需要定义三个构造函数,下面便是某个自定义视图的Java代码例子:

public class CustomView extends View {
    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context,AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

对于上述这种存在多个构造函数的情况,Java可以通过覆写带不同参数的构造函数来实现,那么Kotlin已经在类名后面指明了固定数量的入参,又该如何表示拥有其它参数的构造函数?针对这个疑点,Kotlin引入了主构造函数与二级构造函数的概念,上面演示的只是主构造函数,它分为两部分:跟在类名后面的参数是主构造函数的入参,同时init方法是主构造函数的内部代码。至于二级构造函数,则可以在类内部直接书写完整的函数表达式,为了让读者有更直观的认识,下面先贴出一段包含二级构造函数的Kotlin类定义代码:

class AnimalMain constructor(context:Context, name:String) {
    init {
        context.toast("这是只$name");
    }

    constructor(context:Context, name:String, sex:Int) : this(context, name) {
        var sexName:String = if(sex==0) "公" else "母"
        context.toast("这只${name}是${sexName}的");
    }
}

从上可以看出,二级构造函数和普通函数相比有两个区别:

1、二级构造函数没有函数名称,只用关键字constructor表示这是个构造函数。
2、二级构造函数需要调用主构造函数,“this(context, name)”这句代码在Java中要写在函数体内部,在Kotlin中则以冒号开头补充到输入参数后面,这意味着二级构造函数实际上是从主构造函数扩展而来,冒号表示前边属于后边的类型,犹如“var count:Int”一般。
由此看来,因为二级构造函数从属于主构造函数,于是如果使用二级构造函数声明该类的实例,则系统会先调用主构造函数的init代码,再调用二级构造函数的自身代码。现在若想声明AnimalMain类的实例,即可通过主构造函数声明,也可通过二级构造函数声明,具体的声明代码如下所示:

    btn_class_main.setOnClickListener {
        setAnimalInfo()
        when (count%2) {
            0 -> { var animal = AnimalMain(this, animalName) }
            else -> { var animal = AnimalMain(this, animalName, animalSex) }
        }
    }

不过在测试过程中发现,通过二级构造函数声明实例有个问题,就是toast会弹窗两次。原因是主构造函数的init方法已经弹窗,然后二级构造函数自身再次弹窗,看来这么做并不完美,能否不要强制调用主构造函数呢?为了解决该问题,Kotlin设定了主构造函数不是必需的,也就是说,某个类可以把几个构造函数都放在类内部定义,就去掉了主构造函数,据此修改之后的类代码如下:

class AnimalSeparate {
    constructor(context:Context, name:String) {
        context.toast("这是只$name");
    }

    constructor(context: Context, name:String, sex:Int) {
        var sexName:String = if(sex==0) "公" else "母"
        context.toast("这只${name}是${sexName}的");
    }
}

这样一来,新类AnimalSeparate便不存在主构造函数了,它的两个二级构造函数之间没有从属关系,它们各自的函数代码是互相独立的。无论通过哪个构造函数声明类的实例,都只会调用这个构造函数的代码,而不会像之前那样去调用主构造函数的代码了。

未料如此折腾一番,隐隐感觉哪里不对劲,猛然发现改来改去,AnimalSeparate类依旧完整写着两个构造函数,这么做跟Java的构造函数写法又有什么区别呢?无非是把类名换成了关键字constructor,其它地方仍然换汤不换药。Kotlin的宗旨是化繁为简,没想到结果却返璞归真了,真是令人吓出一身冷汗。客官莫急,倘若Kotlin黔驴技穷,那么它根本没资格挑战Java,所以肯定是有办法的。不知读者是否还记得前面介绍函数时说到的默认参数?类的构造函数同样也能添加默认参数。
注意到AnimalSeparate类的两个构造函数只是相差一个输入参数,所以完全可以把它们合并成一个带默认参数的主构造函数,新的主构造函数既可以输入两个参数,又可以输入三个参数。如果利用带两个入参的主构造函数创建实例,则形同调用了原来的第一个构造函数“constructor(context:Context, name:String)”;如果利用带三个入参的主构造函数创建实例,则形同调用了原来的第二个构造函数“constructor(context: Context, name:String, sex:Int)”。下面即为采取默认参数的类定义代码:

//类的主构造函数使用了默认参数
class AnimalDefault (context: Context, name:String, sex:Int = 0) {
    init {
        var sexName:String = if(sex==0) "公" else "母"
        context.toast("这只${name}是${sexName}的");
    }
}

这下看起来简洁了许多,新类AnimalDefault用起来也毫不费事,之前的实例创建代码只消换个类名就好,完全无缝对接。具体的调用代码如下所示:

    btn_class_default.setOnClickListener {
        setAnimalInfo()
        when (count%2) {
            0 -> { var animal = AnimalDefault(this, animalName) }
            else -> { var animal = AnimalDefault(this, animalName, animalSex) }
        }
    }

总结一下,Kotlin给类的构造函数引进了关键字constructor,并且区分了主构造函数和二级构造函数。主构造函数的入参在类名后面声明,函数体则位于init方法中;二级构造函数从属于主构造函数,它不但由主构造函数扩展而来,而且必定先调用主构造函数的实现代码。另外,Kotlin的构造函数也支持默认参数,从而避免了冗余的构造函数定义。

__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。

时间: 2024-10-14 22:12:36

Kotlin入门(12)类的概貌与构造的相关文章

IOS开发语言Swift入门连载---类和结构体

IOS开发语言Swift入门连载-类和结构体 类和结构体是人们构建代码所用的一种通用且灵活的构造体.为了在类和结构体中实现各种功能,我们必须要严格按照常量.变量以及函数所规定的语法规则来定义属性和添加方法. 与其他编程语言所不同的是,Swift 并不要求你为自定义类和结构去创建独立的接口和实现文件.你所要做的是在一个单一文件中定义一个类或者结构体,系统将会自动生成面向其它代码的外部接口. 注意: 通常一个类 的实例被称为对象 .然而在Swift 中,类和结构体的关系要比在其他语言中更加的密切,本

Kotlin入门(32)网络接口访问

手机上的资源毕竟有限,为了获取更丰富的信息,就得到辽阔的互联网大海上冲浪.对于App自身,也要经常与服务器交互,以便获取最新的数据显示到界面上.这个客户端与服务端之间的信息交互,基本使用HTTP协议进行通信,即App访问服务器的HTTP接口来传输数据.HTTP接口调用在Java代码中可不是一个轻松的活,开发者若用最基础的HttpURLConnection来编码的话,至少要考虑以下场景的处理:1.HTTP的请求方式是什么,是GET还是POST还是PUT还是DELETE?2.HTTP的连接超时时间是

c#图像处理入门(-bitmap类和图像像素值获取方法)

c#图像处理入门 -bitmap类和图像像素值获取方法 一.Bitmap类 Bitmap对象封装了GDI+中的一个位图,此位图由图形图像及其属性的像素数据组成.因此Bitmap是用于处理由像素数据定义的图像的对象.该类的主要方法和属性如下: 1. GetPixel方法和SetPixel方法:获取和设置一个图像的指定像素的颜色. 2. PixelFormat属性:返回图像的像素格式. 3. Palette属性:获取和设置图像所使用的颜色调色板. 4. Height Width属性:返回图像的高度和

Java入门——Runtime类

Java入门——Runtime类 认识Runtime类 Runtime类表示运行时的操作类,是一个封装了JVM进程的类,每一个JVM都对应一个Runtime类的实例,此实例由JVM运行时为其实例化.取得Runtime类实例的方法为: Runtime run=Runtime.getRuntime(): 可以通过Runtime实例取得系统的一些信息 package Sep26; public class RuntimeDemo01 { public static void main(String[]

C++语言笔记系列之十五——派生类、基类、子对象的构造和析构函数调用关系

例子 example 1 注:若一个基类同时派生出两个派生类,即两个派生类从同一个基类继承,那么系统将为每一个简历副本,每个派生类独立地使用自己的基类副本(比如基类中有属于自己类的静态变量等). #include <iostream.h> class Person { public: person() {cout<<"Construction of person."<<endl;} ~person() {cout<<"Destr

Java入门——System类

Java入门——System类 System类简介 System类是一些与系统相关的属性和方法的集合,而且所有属性都是静态的. 序号 方法定义 类型 描述 1 public static void exit(int static) 普通 系统退出,如果status为非0就表示退出 2 public static void gc() 普通 运行垃圾回收机制,调用的是Runtime类的gc()方法 3 public static long currentTimeMills() 普通 返回以毫秒为单位

OC_语法入门_day1_类的定义

H:/Objective-C/OC_day0/00-OC经典入门.m /*================第一段代码(start)========================== = #import <Foundation/Foundation.h> // 1,类的声明 @interface Car : NSObject { // 所有成员变量,默认初始值均为0 @public int wheels; int speed; } // 对象的方法 // 1,前面固定写减号- // 2,不同于

Java入门——StringBuffer类

Java入门——StringBuffer类 认识StringBuffer类 如果一个字符串需要经常被改变,就需要使用StringBuffer类.(String类型的变量一旦声明就很难改变,若想改变,必须改变引用地址)! 字符串的连接操作 在程序书中使用append方法可以进行字符串的连接操作. package Sep22; public class StringBufferDemo01 { public static void main(String[] args) { StringBuffer

Kotlin入门

转载自:https://www.cnblogs.com/jaymo/articles/6924144.html 创建类的实例 要创建一个类的实例,我们就像普通函数一样调用构造函数: 1 2 3 val invoice = Invoice() val customer = Customer("Joe Smith") 注意 Kotlin 并没有 new 关键字. 继承 在 Kotlin 中所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类 1 class Example