Kotlin真香系列第二弹:类型初探

目录

写在前面

一、类和接口

1.1、类

1.2、接口

1.3、抽象类

1.4、属性

1.5、属性引用

二、扩展方法

2.1、扩展方法

2.2、扩展属性

三、空类型安全

3.1、空类型安全的概念

3.2、空类型的继承关系

3.3、平台类型

四、智能类型转换

4.1、智能类型转换的用法

4.2、不支持智能转换的情况

4.3、类型的安全转换

五、综合案例——使用Retrofit发送网络请求


写在前面

上一篇中我们介绍了Kotlin基础的数据类型——《Kotlin真香系列第一弹:内置类型》,今天继续沿着这条Kotlin学习之路往下走,来了解一些类型里面的其它概念,比如空类型、智能类型转换,当然了必不可少的类的概念在这一篇当中我们也要涉及了,是不是有点期待了,嗯。。。,那就开始吧!

一、类和接口

关于类和接口,其实这个也没啥新鲜的了,因为咱们都是有Java基础的,所以可以很容易理解这些概念,因为Kotlin中的类和Java中的其实是一样的概念,只不过写法上有些不同而已。

1.1、类

①、类的定义

Java中的类定义是这样的:

Kotlin中的类定义是这样的:

上面展示的是最简单的定义方式,咱们再来看个稍微复杂点的,里面定义了变量和函数:

接着来看构造方法:构造器constructor,如果你实在不知道怎么写就按照下面这种最原始的方式来定义构造方法,这个构造器又被叫做副构造器:secondary constructor,如下图所示:

当然这种定义构造方法的方式咱们可以简化,咱们可以把constructor给它移到类的定义那一行,这个构造器又被叫做主构造器:primary constructor,主构造器要求其它所有的构造器都必须调用它,如下图所示:

上图中的构造器都由副转正了,你都放在类名后面了,谁还能不知道你是主构造器啊,你还弄个constructor是不是显得有点多余了,所以再简化一步可以直接写成下面这种格式:

嗯不错,上面图中的这种方式看着就简洁多了,不过你以为到这里就完了吗?当然不是,还可以更简单,x这个属性是在类中定义的,然后它是使用了构造器的参数来进行初始化,所以你就可以直接简写成下面这种格式,如果在构造器中一旦加上了var或者val,那么就相当于我们是定义了一个属性:

实际上一个类的主要构成就是属性和行为,也就是我们在代码中定义的成员变量和函数,所以你知道怎么定义成员的,又知道怎么定义函数的,一结合那么牛就知道怎么定义类了,所以没啥复杂的东西,都是你之前就已经掌握的概念,换个写法而已。

②、类的实例化

单身狗注意了,在Kotlin里面没对象的也不会给你new一个对象的机会了,这一点跟Java不一样喽:

1.2、接口

①、接口的定义

②、接口的实现

1.3、抽象类

①、抽象类的定义

Kotlin里面抽象类中的方法如果被abstract关键字标记为抽象方法的是肯定要被复写的,但是其它的方法定义出来默认就是final的不可以被复写,如果想要让某个方法可以被复写,则需要显示的加上open关键字,在需要复写的时候对应方法前加上override关键字

②、类的继承

抽象类的继承跟接口的实现有点类似,只不过这里多了一个括号,它表示是调用了父类的构造方法,如果没有明确的定义出来的话,会有一个默认的无参构造器

到这里,咱们先停一下,先把上面讲的这几个东西通过实例来看一下具体怎么使用:

定义抽象类:

abstract class AbsClass {
    abstract fun absMethod()
    open fun overridable(){}
    fun nonOverridable(){}
}

定义接口:

interface SimpleInf {
    val simpleProperty: Int // property

    fun simpleMethod() //function
}

定义实现类:

//类的定义,继承AbsClass这个抽象类,实现SimpleInf接口
open class SimpleClass(var x: Int, val y: String) : AbsClass(), SimpleInf {

    //复写的抽象方法
    override fun absMethod() {}

    //实现接口的属性
    override val simpleProperty: Int
        get() {
            return 5
        }

    //实现接口的方法
    override fun simpleMethod() {}

    //可以复写的抽象类中的open方法
    override fun overridable() {}
//    final override fun overridable() {}

    //定义成员,实现get()方法,做一个乘法运算
    val z : Long
        get() {
            return simpleProperty * 5L
        }

    //定义普通方法
    fun yyy(){
        println("The function name is yyy")
    }

    fun zzz(string: String){
        println("The property name is $string")
    }
}

//定义SimpleClass2继承SimpleClass,则SimpleClass需要声明为open类型
class SimpleClass2(x: Int, y: String) : SimpleClass(x, y) {
    override fun overridable() {} //可以继续复写,如果定义为final则不能再复写了
}

创建测试程序main():

fun main() {
    val simpleClass = SimpleClass(88,"Jarchie")
    println(simpleClass.x)
    println(simpleClass.y)
    println(simpleClass.z)
    simpleClass.yyy()
    simpleClass.zzz("Simple")
}

执行结果如下:

1.4、属性

Kotlin中的getter和setter需要你自定义,不自定义的话它会帮你默认定义好setter和getter,也就是说在Java中你定义一个field它就只有一个值在里面,在Kotlin里面如果你只定义了一个property,它就是一个值加上一个setter和getter,无需你自己去定义setter和getter方法,如果是只读的val,那么就没有setter了,下面图中就是一个典型的JavaBean,左侧定义的变量age以及它的setAge()和getAge()方法,在右侧Kotlin中你就可以只写var age:Int = age,这里也写出来了,是为了让大家明白你不写它也会自动给你生成的,这个field和左侧的age是完全对应的关系:

1.5、属性引用

上一篇中说了函数有引用,那么其实属性也有引用,并且拿到的引用值也是可以进行set和get的,需要注意的是它和函数引用存在一个同样的问题,未绑定receiver的时候,比如下图中的ageRef,在set时需要主动传入一个receiver进去,如果在创建的时候就绑定了receiver的话,比如下图中通过对象实例获取属性的引用,再次set时就不需要传入receiver了:

针对于属性咱们也来通过一个实例看一下具体的使用吧:

fun main() {
    val ageRef = Person::age //获取age的引用,未绑定Receiver的情况
    val person = Person(18, "jianqi") //实例化
    val nameRef = person::name //获取name的引用,并且绑定Receiver,通过对象的实例获取的
    ageRef.set(person, 20) //需要传入receiver
    nameRef.set("Jarchie") //无需传入receiver
    println(ageRef.get(person))
    println(nameRef.get())
}

class Person(age: Int, name: String) {
    var age: Int = age //property
        get() {
            return field
        }
        set(value) {
            println("setAge: $value")
            field = value
        }
    var name: String = name
        get() {
            return field // backing field
        }
        set(value) {

        }
}

执行结果如下:

二、扩展方法

2.1、扩展方法

我们之前使用Java进行开发的时候,想必大家手里面肯定都写过许许多多的工具类比如StringUtils等等,为什么会出现这么多的Utils呢?其实啊是因为jdk本身开发的时候考虑的情况可能不够,进而就导致了我们开发的过程中发现有些方法是非常实用的,都是开发必备的,所以我们就写了很多的Utils,只不过可能看起来不是太美观。那有没有什么方法可以让我们在后期开发的时候给一些类比如说String这种已有的类提供一些其它你需要的方法呢?嗯,Kotlin语言在设计的时候就考虑到了这个问题,所以在Kotlin中是可以的。

举个栗子来说明一下吧,这样感觉应该能容易理解一点,比如我们在main函数中想判断它的入参是否为空该咋判断呢?

首先看Java中的写法:它里面没有直接的写法,只能通过args.length是否等于0来判断:

所以为了方便呢,咱们通常会把它抽出来抽成一个工具方法:

public class Utils {
    public static boolean isEmpty(String[] args) {
        return args.length == 0;
    }

    public static boolean isNotEmpty(String[] args) {
        return args.length > 0;
    }
}

然后再到main函数中去使用:

public class ExtendsJava {
    public static void main(String[] args) {
        if (Utils.isEmpty(args)){
            
        }
    }
}

这个时候你是不是有这样的想法呢:如果args要是能直接args.isEmpty()该多happy啊!哎,年轻人有想法挺好的,你还别说,在Kotlin里面咱们还真就可以这么干,嗯,Kotlin真香:

其实啊,这个isEmpty()方法并不是直接定义在Array里面的,不信是吗?不信你就点进去看一下,它其实是定义在一个_Arrays.kt文件里的,这实际上就是一个扩展方法了:

扩展方法的定义跟普通方法的定义非常的类似,它们的差别主要是方法名前面多了个 什么点 即:X.isEmpty,X->Array<out T>,

谁点的方法那么方法体里面的this就是谁,这就相当于方法体内部就是 return this.size == 0。

下面咱们自己来定义一个扩展方法吧,这玩意还是挺有意思的,一起来动手敲敲,举个栗子:

在控制台输出10个连续的“乔布奇”这样的连着的字符串,并且实现通用性,何为通用性,就是你随便传入一个什么字符串它都能输出连续的指定个数的字符串,思考一下该如何实现?下面就直接实现一把,体验一下扩展方法的好玩的地方:

fun main() {
    println("乔布奇".continuousReplication(10))
    println("爸爸".continuousReplication(10))
    println("乔布奇" * 10) //纳尼,还能这么干,牛牪犇逼
}

//定义扩展方法
fun String.continuousReplication(num: Int): String {
    val stringBuilder = StringBuilder()
    for (i in 0 until num) {
        stringBuilder.append(this)
    }
    return stringBuilder.toString()
}

//运算符重载
operator fun String.times(num: Int): String {
    val stringBuilder = StringBuilder()
    for (i in 0 until num) {
        stringBuilder.append(this)
    }
    return stringBuilder.toString()
}

执行结果:

OK,上面的就是扩展方法的一个使用案例了,其实还是挺简单的吧,就是加了一个“什么点”就行了,一旦加了这玩意之后,里面的this就指代调用者,上面的例子中调用者就是“乔布奇”。

那咱们定义的这个方法在Java中该怎么用呢?大家都知道Java和Kotlin是可以互相调用和转化的,所以这个方法肯定也是可以直接使用的,并且也是挺简单滴:

2.2、扩展属性

除了扩展方法,其实扩展属性也是可以的,写法也都差不多,与类内部直接定义属性不同的是它没有backing field,这个就跟咱们在接口中定义属性是一样的,所以只能通过定义相应的setter跟getter方法才行。

举个栗子吧,比如咱们给String加一个扩展属性sb咋样,没错就是这么无聊:

fun main() {
    println("SB".sb1)
    println("Test".sb2)
}

val String.sb1 : String
get() = "You"

var String.sb2:Int
set(value) {
//    field = value
    //没有field,看着也很多余哈
}
get() = 222

执行结果如下:

三、空类型安全

3.1、空类型安全的概念

Kotlin中为什么会有空类型安全的概念呢?因为咱们写代码的时候很容易会出现一个很恶心的异常:空指针异常!如下图中所示:定义了nonNull这个变量,它的类型是String,这个String跟Java中的String还是有一定的区别的,这个String类型表示不能接受空值,所以第二行如果你赋值为null,编译器是不允许你编译通过的,因为它是不可空类型:

现在我们把赋值为空的这一行代码给注掉,来获取一下这个字符串的长度:

但是在Java中定义一个String类型的变量的时候你是不确定这个变量是否为空的,并且是可以给它赋值为空的,所以Kotlin为了100%的兼容Java,那它就必须得能够接收空啊,那怎么办呢?这么办,我在定义的时候给它加一个问号?,任何类型都是可以的哦,在后面加?表示该类型可以为空:

现在定义是没问题了,但是在使用的时候问题又来了,比如下面这段代码,因为你这个nullable是可能为空的,所以你在获取它的长度的时候,编译器不确定你是否为空,就只能给你编译不通过了:

那我要获取个字符串长度还不给我用了可咋办呢?别急,咱可以强制的告诉编译器我这个string啊确定是不为空的:!!,俩感叹号,这是个类型强转,表示强转为不可空类型:

当然了你现在是能确定这个nullable是不可能为空的,因为它里面有值“Hello”,所以你可以使用这种强转的方式,虽然有点暴力了啊,但是也说得过去。可是如果你并不知道某个值是否为空那咋办呢?你可以使用“?”这种方式:

但是这样虽然编译器也能通过,不过这个值是它不确定是否为空的,如果为空了那还是会出现问题的,那咱们肯定是想要保证程序的稳定性喽,像空指针这种异常应该给它消灭在胚胎里,所以又有了新的解决方案,使用elvis表达式,即“?:”问号冒号这种形式,它有点类似于咱们Java里面的三目运算符,就是如果为空会给它一个默认值,保证安全性:

3.2、空类型的继承关系

首先来看下面这张图:我们知道Int是Number的子类,所以b=a是可以的

那么同样的道理,我们来写一下String和String?:

这个结果就很容易能够看出来了:String?包含String,也就是说String应该是String?的一个子类。

3.3、平台类型

Kotlin刚出来的时候所有的平台都是指向Java的字节码的,但是现在随着Kotlin的升级,自身越来越牛,它还可以编译成JavaScript和机器码,所以现在平台类型不仅指Java字节码也指JavaScript和Native的类型:

下面来举个栗子说明一下这个平台类型究竟是什么玩意:

首先我们新建一个Java类,并且定义了一个getTitle()方法:

public class PlateformJava {
    public String getTitle(){
        return null;
    }
}

然后我们创建一个Kotlin文件,写一个main函数,在函数中调用这个Java类中的title属性:

fun main() {
    val plateformJava = PlateformJava()
    val title = plateformJava.title
}

这里的plateformJava.title实际上调用的就是Java类里面的getTitle()方法,那咱们在Kotlin中定义的这个title是什么类型呢?查看一下吧,快捷键ctrl+shift+p:

哎呦喂,String!这是个什么类型啊,这其实就是个平台类型,平台类型是客观存在却不能主观定义的一种特殊存在,不信你在代码中声明一下这个类型试试看,你写都写不出来,编译器根本不给你识别,会报错的:

平台类型存在于Java或者JavaScript或者Native当中,取决于你的Kotlin跑在什么平台当中,我们在是跑在JVM上面所以这里的平台类型就是Java的类型,我现在直接调用Java的方法,那么实际上调用的就是Java的getTitle()方法了:

那接着我们再来写一行代码来获取一下这个title的长度:

这段代码写完会出现什么相信不用我多说了吧,它一定会出现空指针异常,因为你Java代码里面返回的是个null啊:

那有些朋友可能已经发现问题了,这里明明是null,为什么我们直接.length没报错啊?对,就是这个问题了,其实归根结底就一句话,记清楚了哈:Kotlin的编译器无法推断出平台类型是否为空,需要开发者自行确定是否为空,所以针对于平台类型,咱们在写代码的时候就一定要仔细仔细再仔细了!

四、智能类型转换

4.1、智能类型转换的用法

首先来看一段Java代码,定义了一个Kotliner接口,然后定义了一个Person类来实现这个接口:

然后用子类的实例去赋值给父类或者接口的引用,在用的时候我判断了如果kotliner是一个Person的话,我就把它的名字打印出来,我们在用Java写的时候你会发现这样一个问题,明明已经判断了kotliner是Person,为什么下面还要进行一次强制转换呢?

同样的代码咱们在Kotlin中类比过来如下:

但是Kotlin很聪明啊,它知道你已经判断过了,所以就没必要再强转一次了,所以可以直接这么写:

上面的就是Kotlin的智能类型转换了,关于智能类型转换我们再来举个例子吧:

关于空类型安全,从可空类型转到不可空类型实际上是一个从父类到子类转换的过程,如果你能确定value不等于空,那么在if分支里面就可以直接使用value.length获取字符串长度,这个操作是很安全的,因为编译器自动帮我们做了一次类型的转换:

但是这个安全的作用范围需要注意一下:

4.2、不支持智能转换的情况

如下图中所示,我们定义了一个全局变量tag,然后在main()函数中可以访问到,此时虽然你在if分支中判断过了不为空,但是里面仍然不能直接使用,为什么?因为tag这玩意是全局变量啊,那就意味着任何线程都可以访问到它,所以直接使用是不能保证安全性的,所以这种场景下智能安全转换就不生效了,需要格外注意这一点:

4.3、类型的安全转换

上面在定义变量的时候咱们对于可空类型会加个“?”来保证安全性,实际上类型转换的时候它也弄了个这玩意:

所以(Kotliner as? Person)这个表达式整体的类型是一个Person?的类型,如果转换成功就返回Person,转换失败则返回null。

由此也说明这种访问name的方式是不安全的,所以对于访问name我们也要使用安全的访问方式:

关于智能类型转换总结几点需要注意的地方:

  • 尽可能使用val来声明不可变引用,让程序的含义更加清晰确定
  • 尽可能减少函数对外部变量的访问,为函数式编程提供基础
  • 必要时创建局部变量指向外部变量,避免因它变化引起程序错误

五、综合案例——使用Retrofit发送网络请求

这一部分我们来写个综合案例,实现的主要功能就是仿照平时Android开发的时候使用的Retrofit2网络库发送一个网络请求,然后拿到返回的response json串,然后将它保存到一个.html文件中。那接下来就一步一步的实现吧:

①、添加依赖

    //Retrofit
    implementation "com.squareup.retrofit2:retrofit:2.6.2"
    implementation "com.squareup.retrofit2:converter-gson:2.6.2"
    //Gson
    implementation "com.google.code.gson:gson:2.8.5"

然后准备好需要请求的地址和json串:

请求地址:https://api.github.com/repos/JetBrains/Kotlin

返回json:由于返回的内容太长了,我这里就不全部贴出来了,大家将上面的地址放到浏览器中打开就可以看到返参了:

②、创建实体类

我们之前使用Java开发的时候,在Android Studio中生成实体类都是直接使用GsonFormat这个插件的,将json串复制到插件窗口中它会自动帮我们生成实体类,那现在使用Kotlin该咋办呢?一行一行的还不得累死了啊,放心,哪里有反抗哪里就有压迫,给大家推荐一个插件NewDataClassAction,在plugins里面搜索然后安装重启IDE即可:

安装完成之后咱们选择New--->New Data Class,然后填入类名和json串,它就可以帮咱们自动生成实体类了:

这个data关键字后面会讲,它是Kotlin中的数据类:

data class Repository(
    var id: Int,
    var node_id: String,
    var name: String,
    var full_name: String,
    var private: Boolean,
    var owner: Owner,
    var html_url: String,
    var description: String,
    var fork: Boolean,
    var url: String,
    var forks_url: String,
    var keys_url: String,
    var collaborators_url: String,
    var teams_url: String,
    var hooks_url: String,
    var issue_events_url: String,
    var events_url: String,
    var assignees_url: String,
    var branches_url: String,
    var tags_url: String,
    var blobs_url: String,
    var git_tags_url: String,
    var git_refs_url: String,
    var trees_url: String,
    var statuses_url: String,
    var languages_url: String,
    var stargazers_url: String,
    var contributors_url: String,
    var subscribers_url: String,
    var subscription_url: String,
    var commits_url: String,
    var git_commits_url: String,
    var comments_url: String,
    var issue_comment_url: String,
    var contents_url: String,
    var compare_url: String,
    var merges_url: String,
    var archive_url: String,
    var downloads_url: String,
    var issues_url: String,
    var pulls_url: String,
    var milestones_url: String,
    var notifications_url: String,
    var labels_url: String,
    var releases_url: String,
    var deployments_url: String,
    var created_at: String,
    var updated_at: String,
    var pushed_at: String,
    var git_url: String,
    var ssh_url: String,
    var clone_url: String,
    var svn_url: String,
    var homepage: String,
    var size: Int,
    var stargazers_count: Int,
    var watchers_count: Int,
    var language: String,
    var has_issues: Boolean,
    var has_projects: Boolean,
    var has_downloads: Boolean,
    var has_wiki: Boolean,
    var has_pages: Boolean,
    var forks_count: Int,
    var mirror_url: Any,
    var archived: Boolean,
    var disabled: Boolean,
    var open_issues_count: Int,
    var license: Any,
    var forks: Int,
    var open_issues: Int,
    var watchers: Int,
    var default_branch: String,
    var temp_clone_token: Any,
    var organization: Organization,
    var network_count: Int,
    var subscribers_count: Int
) {
    data class Owner(
        var login: String,
        var id: Int,
        var node_id: String,
        var avatar_url: String,
        var gravatar_id: String,
        var url: String,
        var html_url: String,
        var followers_url: String,
        var following_url: String,
        var gists_url: String,
        var starred_url: String,
        var subscriptions_url: String,
        var organizations_url: String,
        var repos_url: String,
        var events_url: String,
        var received_events_url: String,
        var type: String,
        var site_admin: Boolean
    )

    data class Organization(
        var login: String,
        var id: Int,
        var node_id: String,
        var avatar_url: String,
        var gravatar_id: String,
        var url: String,
        var html_url: String,
        var followers_url: String,
        var following_url: String,
        var gists_url: String,
        var starred_url: String,
        var subscriptions_url: String,
        var organizations_url: String,
        var repos_url: String,
        var events_url: String,
        var received_events_url: String,
        var type: String,
        var site_admin: Boolean
    )
}

③、定义接口

这是一个Get请求,路径上面也提供了,定义的方式不多说和使用Java的时候没多大区别,可以说是基本一致:

interface GitHubApi {
    @GET("/repos/{owner}/{repo}")
    fun getRepository(@Path("owner") owner: String, @Path("repo") repo: String): Call<Repository>
}

④、实现main()函数

fun main() {
    //创建api
    val gitHubApi = Retrofit.Builder().baseUrl("https://api.github.com")
        .addConverterFactory(GsonConverterFactory.create()) //gson解析
        .build()
        .create(GitHubApi::class.java)
    //拿到response,这里使用的是同步请求
    val response = gitHubApi.getRepository("JetBrains", "Kotlin").execute()
    //获取response的body
    val repository = response.body()

    if (repository == null) {
        println("请求失败! ${response.code()} - ${response.message()}")
    } else {
        println(repository.name)
        println(repository.owner.login)
        println(repository.stargazers_count)
        println(repository.forks_count)
        println(repository.html_url)

        //File()是Java中的File,writeText是扩展方法,将数据写入Kotlin.html文件中
        File("Kotlin.html").writeText(
            """
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="UTF-8">
                <title>${repository.owner.login} - ${repository.name}</title>
            </head>
            <body>
                <h1><a href='${repository.html_url}'>${repository.owner.login} - ${repository.name}</a></h1>
                <p>${repository.description}</p>
                <p>Stars: ${repository.stargazers_count}</p>
                <p>Forks: ${repository.forks_count}</p>
            </body>
            </html>
        """.trimIndent()
        )
    }
}

Retrofit的使用方式大家肯定一看就懂了,跟Java中没有多大区别,只不过这里使用的是同步请求并非异步的,然后获取到json之后,将部分字段内容写入到了一个Html文件中。

⑤、执行结果

然后我们的目录中也生成了一个Kotlin.html文件,并且你可以直接在浏览器中打开:

上面这些就是今天的主要内容了,主要介绍了类的概念、扩展方法和属性、空类型安全、智能类型转换等知识点,今天就先到这里了,咱们下期再会!

祝:工作顺利!

猜你喜欢

转载自blog.csdn.net/JArchie520/article/details/107347525