Kotlin "谈" "弹" "潭"

Kotlin "谈" "弹" "潭"

本篇针对使用Java的Android开发者,快速入手Kotlin,期间可能啰啰嗦嗦稀里糊涂缓缓乎乎穿插一些我中过的坑。这里不讲Kotlin异步并发(协程)、不讲Kotlin反射,如果你是来看它们的。那我现在也木有。

image

目录

一、为什么要学习kotlin
二、基本使用
1、Java写法和Kotlin的写法对比
2、基本类型
  • Kotlin中数字类型
  • 运算符号
  • 布尔值
  • 数组
3、基本表达式
  • 表达式
  • 流程控制
  • when使你力腕狂澜
  • 使用表达式比语句更加安全
  • for循环的奥义
4、各种类
  • 类、接口基本概念
  • 伴生对象
  • 单例类
  • 匿名内部类
  • 数据类
5、各种函数
  • lambda表达式
  • 内联函数
  • 扩展函数
  • 方法默认参数
6、各种集合
  • List
  • Set
  • Map
  • 集合操作
  • 惰性求值和序列
7、函数方法值类型
8、高阶函数
9、空安全
10、关于设计模式和Kotlin
  • 工厂模式
  • 观察者和代理模式
11、快速对比Java代码必备技能
三、参考

一、为什么要学习kotlin

  • 1、Google推荐
    现在的新特性文档以及例子有一些是只有kotlin的了

  • 2、更加简洁的语法减少啰嗦代码

  • 3、都是基于JVM编译成字节码,与Java基本兼容基本没有太大问题,因此可以用java写的很多库,生态强大

  • 4、强大的语言特性

二、基本使用

1、Java写法和Kotlin的写法对比

from-java-to-kotlin

2、基本类型

Kotlin中所有的东西都是对象,包括基本类型,一般来说数字、字符、布尔值、数组与字符串是组成一门语言的基本数据类型。

Kotlin中数字类型
Type Bit width
Double 64
Float 32
Long 64
Int 32
Short 16
Byte 8

每个数字类型支持如下的显示转换,比如

  • toByte(): Byte
  • toShort(): Short
  • toInt(): Int
  • toLong(): Long
运算符号
  • shl(bits) – 有符号左移 (Java 的 <<)
  • shr(bits) – 有符号右移 (Java 的 >>)
  • ushr(bits) – 无符号右移 (Java 的 >>>)
  • and(bits) – 位与
  • or(bits) – 位或
  • xor(bits) – 位异或
  • inv() – 位非
布尔值

Boolean 类型,有两个值true 与 false

数组

数组在 Kotlin 中使用 Array 类来表示,它定义了 get 与 set 函数(按照运算符重载约定这会转变为 [])以及 size 属性

比如说

val persons = Array<Person>(3) {
            Person("2")
            Person("3")
            Person("4")
        }
persons.size

当然,kotlin也支持简单的基本原生基本类型,无装箱开箱的开销的ByteArray、 ShortArray、IntArray 等等

val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]

3、基本表达式

表达式

什么是表达式呢,在Java中,以;结尾的一段代码,即为一个表达式。

setContentView(R.layout.activity_main)

public static final int CAT_DRIVERS = 20;

Log.e(TAG, "猫司机的腿数量: " + catCount * 3);

这个也是kotlin中表达式的概念。

流程控制

Kotlin的流程控制和Java的基本相同,最大的区别是Kotlin没有switch语句,kotlin中是更加强大的when语句。而且kotlin的if语句和when语句可以当做表达式来使用,就跟Java中的三目运算符一样。

Java:
String name = isOne ? "是一个人" : "是半个人";

可以说when是Java中if和switch的一次强大的联合。比如说,可以有这种写法

var single3 = when (single) {
        0, 1 -> aLog("single == 0 or single == 1")
        else -> aLog("其他")
    }

if和while都是和java一样的,区别在于if可以当做表达式来使用,比如

private val single2 = if (single >= 3) {
        aLog("大于等于3")
    } else {
        aLog("小于3")
    }
when使你力腕狂澜

如果很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔:

when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}

我们可以用任意表达式(而不只是常量)作为分支条件

when (x) {
    parseInt(s) -> print("s encodes x")
    else -> print("s does not encode x")
}

我们也可以检测一个值在(in)或者不在(!in)一个区间或者集合中:

when (x) {
    in 1..10 -> print("x is in the range")
    in validNumbers -> print("x is valid")
    !in 10..20 -> print("x is outside the range")
    else -> print("none of the above")
}

另一种可能性是检测一个值是(is)或者不是(!is)一个特定类型的值。注意: 由于智能转换,你可以访问该类型的方法与属性而无需任何额外的检测。

fun hasPrefix(x: Any) = when(x) {
    is String -> x.startsWith("prefix")
    else -> false
}

when 也可以用来取代 if-else if链。 如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支:

when {
    x.isOdd() -> print("x is odd")
    x.isEven() -> print("x is even")
    else -> print("x is funny")
}

这些用法,可以说一个when让你不再写出混乱的if嵌套。

使用表达式比语句更加安全

先看一段Java代码

private void dididada(boolean needInit) {
    String a = null;
    if(needInit){
        a = "a is Dog that not girlfriend";
    }
    Log.e(TAG, a.toString());
}

这段代码可能有潜在的问题,比如说a必须在if语句外部声明,它被初始化为null。这段代码中,我们忽略了else分支,如果needInit为false,那么会导致a.toString()空指针异常问题。并且如果needInit的值时很深的路径传递过来的,那么可能会导致这个问题更容易被忽略。

如果,你用了表达式


image

比如你这样子写了


fun dididada(boolean needInit) {
    String a = if(needInit){
            "a is Dog that not girlfriend"
    } else{
        ""
    }
    Log.e(TAG, a.toString())
}

实际上,可以更简化点

fun dididada(boolean needInit) {
    String a = if(needInit) "a is Dog that not girlfriend" else ""
    Log.e(TAG, a.toString())
}

因为表达式强制需要你写出else语句,也就不再存在上面说的漏掉else的问题了。

for循环的奥义

上面加when的时候出现的..关键字和in关键字这些,..这个叫做区间,比如说1..4代表从1到4的数列。比如说

if (i in 1..4) {  // 等同于 1 <= i && i <= 4
    print(i)
}

for (i in 1..4) {
    print(i)
}
输出:1,2,3,4  

如果你要倒序呢,你可以这样

for (i in 4 downTo 1) {
    print(i)
}
输出:4,3,2,1  

downTo关键字代表反向遍历,如果你想输出一个等差数列,你可以用step关键字

for (i in 1..8 step 2){
    print(i)
}
输出:1,3,5,7

用Java写出类似的逻辑,你需要

for (int i = 1; i <= 8; i += 2) {
  print(i)
}

可见Kotlin相对于Java的简洁

刚才上面所说的区间都是左闭右也闭的,你还可以使用until来代替..实现左闭右开区间,就比如

for (i in 1 until 10) {       // i in [1, 10), 10 is excluded
    print(i)
}

4、各种类

类、接口基本概念

kotlin中类、接口的声明跟Java中是一致的

class Pig { ... }

interface Motion {
    fun run()
}

如果你要继承一个类或者实现一个接口,都可以使用:符号,另外接口想继承另外一个接口,也是可以使用:来达到目的,可以说是统一extends和implement关键字,有利有弊。
比如Pig继承Animal类和实现Motion接口


class Animal { 
    var name:String?
}

inteface Motion {
    fun run()
}

class Pig : Animal, Motion{
    override fun run() {
        val pigName = name
    }
}

kotlin的类的声明有一个很重要的概念是主构造函数和次构造函数,主构造函数是类头的一部分,它跟在类名的后面,比如

class Pig constructor(name: String) { ... }

你甚至可以省略主构造器的constructor,就像这样

class Pig(name: String) { ... }

什么情况可以省略呢,比如说你不需要给name添加注解或修改可见性的时候,那比如说,如果你想在主构造器初始化的时候在里面写一串初始化逻辑,要怎么样呢?用Java的时候你应该是这样的

class Pig { 
    public Pig(String name) {
        //初始化逻辑
    }
}

但是现在kotlin使用主构造器初始化的话,你在也看不到可爱的构造器方法了


image

莫慌,你还可以这样,kotlin中提供了一种init代码块,它会在调用构造方法的时候按照在类中的init块的顺序从上往下执行,比如

class Pig(name: String) {

    //第一个init块
    init {
        println("${name}")
    }
   
    //第二个init块
    init {
        println("名字长度:${name.length}")
    }
}

执行的时候

Pig("你是猪猪猪", 5)

输出:
你是猪猪猪
名字长度:5

次构造器就跟Java中差不多了,但是不同的地方在于必须要有constructor关键字来声明

class Pig(name:String) {
    
    constructor(name: String, length:Int):this(name) {
        println("次构造器")
    }
    

如果同时有主构造器和次构造器以及init块,他们的执行顺序是什么样的呢?实际上,init块会作为主构造器的一部分。比如

class Pig(name:String) {
     //第一个init块
    init {
        println("${name}")
    }

    constructor(name: String, length:Int):this(name) {
        println("次构造器")
    }
    
    //第二个init块
    init {
        println("名字长度:${name.length}")
    }
}


执行的时候

Pig("你是猪猪猪")

输出:
1.你是猪猪猪
2.名字长度:5
3.次构造器

可见,不管你的init块在哪里,init块会先执行完毕

伴生对象

kotlin中没有静态内部类的概念,取而代之的是伴生对象这货

class Pig {
    companion object Factory {
        fun create(): pig = Pig()
    }
}

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


kotlin中调用
val instance = Pig.create()

Java中调用
Pig pig = Pig.Companion.create()

可见,伴生对象可以让我们轻松实现工厂模式,造猪运动

单例类

平时用到最多的类就是单例类,kotlin自带单例特性,比如

object Demo{
    fun dadada(){
        
    }
}

使用的时候,只需要写Demo.dadada()就能使用这个单例类的方法,当然不要有误解,这个不是静态类的静态方法。反编译kotlin代码

public final class Demo {
   public static final Demo INSTANCE;

   public final void dadada() {
   }

   static {
      Demo var0 = new Demo();
      INSTANCE = var0;
   }
}

可以看到这个是Java中静态内部类版本的单例写法。

当然,如果你想自己写单例,你可以用Java那套自己去撸。比较高端的用法,由于Kotlin有委托属性的概念,可以用lazy属性来写一个懒加载的单例类

class Pig private constructor() {
    companion object {
        val instance: Pig by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        Pig() 
        }
    }
}

等同于Java中的双重校验单例

public class Pig {
    private volatile static Pig instance;
    private Pig(){} 
    public static Pig getInstance(){
        if(instance==null){
            synchronized (Pig.class){
                if(instance==null){
                    instance=new Pig();
                }
            }
        }
        return instance;
    }
}

更多单例的实现可以去Kotlin下的5种单例模式查看

匿名内部类

在Android中,匿名内部类是必不可少的,比如button的点击事件、各个监听事件的事件回调等等。

kotlin中的匿名内部类是要以object开头的,比如Java中的

mExpandMenu.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                
            }
        });

kotlin的写法

view.setOnClickListener(object: View.OnClickListener {
            override fun onClick(v: View?) {
                 //onClick代码块
            }
        })

你可以更精简

view.setOnClickListener {
            //onClick代码块
        }
数据类

我们经常会使用到一些只保存数据的类,java中需要自己去写属性和get和set方法,在kotlin中,可以使用数据类来避免模版代码。

data class User(val name: String, val age: Int)

编译器自动从主构造函数中声明的所有属性导出以下成员:

  • equals()/hashCode() 对;
  • toString() 格式是 "User(name=John, age=42)";
  • componentN() 函数 按声明顺序对应于所有属性;
  • copy() 函数

前三包含了set、get方法和基本的类方法,copy函数实际上是

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

name: String = this.name这种写法是函数默认值写法,后面会单独介绍。

又new了一个对象出来,达到复制的目的。

data class数据类能让我们少写bean类很多代码。

5、各种函数

lambda表达式

上面有个地方其实有提到过kotlin的lambda表达式

view.setOnClickListener(object: View.OnClickListener {
            override fun onClick(v: View?) {
                 //onClick代码块
            }
        })

你可以更精简

view.setOnClickListener {
            //onClick代码块
        }

精简后的其实就是lambda表达式本身,下面我们来看一个例子

val list = listOf<String>()
val filterList = list.filter { it == "pig" }

创建一个List然后过滤出来数据时pig的元素

来看一下filter的源码

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}


public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}

可以看见filter接收的是一个(T) -> Boolean类型的predicate参数,其实它就是lambda类型(Kotlin中所有的东西都是对象,包括这个玩意),表示接收一个T类型的参数返回一个Boolean类型lambda表达式。这里的predicate会一直往下传,直到真正的过滤函数filterTo中,这里写的比较简洁,可以尝试还原,加上大括号

for (element in this) {
    if (predicate(element)) {
        destination.add(element)
    }
}
return destination

其实就是遍历该集合内容,条件是传进来的lambda表达式,我们现在外层传进来的是it == "pig",再度还原,得到如下代码,当元素等于pig的时候,将元素添加到返回的集合中,最后返回新的集合。

for (element in this) {
    if (element == "pig") {
        destination.add(element)
    }
}
return destination

Kotlin中提供了简洁的语法去定义函数的类型,大致如下,当然还有很多变种

() -> Unit//表示无参数无返回值的Lambda表达式类型

(T) -> Unit//表示接收一个T类型参数,无返回值的Lambda表达式类型,这种其实就是上面说的filter

(T) -> R//表示接收一个T类型参数,返回一个R类型值的Lambda表达式类型

(T, P) -> R//表示接收一个T类型和P类型的参数,返回一个R类型值的Lambda表达式类型

(T, (P,Q) -> S) -> R//表示接收一个T类型参数和一个接收P、Q类型两个参数并返回一个S类型的值的Lambda表达式类型参数,返回一个R类型值的Lambda表达式类型

lamda函数在实际使用中,上述列了2种使用场景,很明显是更加简洁易懂,不啰嗦(当然,前提是得知道原理,不然一脸懵),但是也有可能造成调试bug成本增加(难以定位问题代码)。各有利弊

内联函数

上面的filter函数中,有个地方用了inline关键字,其实这个就是内联函数

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

内联函数其实是为了优化lambda开销的产生的,平时我们制造lambda的函数尽量加上inline。具体怎么优化的,可以了解下kotlin是怎么实现lambda函数(本质上是继承Lambda抽象类并且实现了FunctionBase生成的类,比如Function3)(兼容JDK 6的情况下),然后怎么对其做优化的。浅谈Kotlin语法篇之lambda编译成字节码过程完全解析(七)

扩展函数

扩展函数我个人认为是kotlin的最精辟的地方。又回到刚才讲的filter函数,它还有众多的兄弟姐妹,比如map、zip等等等等。但是他们本质上都是扩展函数


public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}


fun Activity.log(content: String) {
    Log.e("minminaya", content)
}

基于这种类型的(扩展的类.扩展函数名字)叫做就是扩展函数。它仅仅限于扩展对象中的方法,扩展函数不能在静态类中使用。我们可以拿这一特性来做很多事情。比如说,ImageView结合Gilde的使用、findViewById、比如工厂方法模式的扩展(后面具体讲)。

以我们项目中常用的Glide工具类为准,这里先看下ImageView这些怎么拓展。

比如说我们之前是将Glide的加载代放到一个工具类里


    public void displayImage(ImageView imageView, String url, RequestOptions requestOptions) {
        if (!isWithValid(imageView)) {
            return;
        }
        Glide.with(imageView.getContext()).asBitmap().apply(requestOptions).load(parseUrl(url)).into(imageView);
    }

然后使用的时候是这样子使用

 GlideLoader.getInstance().displayImage(mIvShareView.getContext(), mIvShareView,
                GlideLoader.wrapFile(imagePath),
                mRequestOptions);

如果将displayImage作为ImageView的扩展函数

public fun ImageView.displayImage(url: String, requestOptions: RequestOptions) {
    if (GlideLoader.isWithValid(this)) {
        return
    }
    Glide.with(context).load(GlideLoader.parseUrl(url)).apply(requestOptions).into(this)
}

使用方式可以变成这样

val imageView = ImageView(this)
        imageView.displayImage("url", RequestOptions())
然后再看下Activity中怎么样消除冗长的findViewById

之前的用法比如像这样

       mIvLongVideoTimeTip = mRootView.findViewById(R.id.iv_long_record_time);
        mLongVideoEffectOptContainer = mRootView.findViewById(R.id.long_record_opt_container);
        mIvLongVideoArEffect = mRootView.findViewById(R.id.iv_selfie_camera_long_video_ar_effect);

如果用扩展函数,你可以这样

fun <T : View> Activity._view(@IdRes id: Int): T {
    return findViewById(id)
}


在Activity中使用:

val tootBar = _view<Toolbar>(R.id.toolbar)

当然kotlin为我们提供了更加方便的findId插件,你只需要在gradle文件中

apply plugin: 'kotlin-android-extensions'

Activity中(比如说Activity的布局是activity_main)

import kotlinx.android.synthetic.main.activity_main.*

比如我们现在的布局xml是这样子写

<androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

那代码中就可以直接使用这个id来对view进行操作

 toolbar.setLogo(R.mipmap.ic_launcher)
方法默认参数

意思就是,方法的参数可以指定默认的值,函数调用的时候,如果没有携带参数,那么使用函数中的默认值,以我们的常用的未重构未Builder模式之前的GuideViewHelper为例,看一下要怎么样用kotlin来重构。

public class GuideViewHelper {

    @Nullable
    public static View showGuideView(Activity activity, final View attachView, int layoutId, final boolean alignView,
                                     final int arrowId) {
        return showGuideView(activity, attachView, layoutId, alignView, false, arrowId);
    }

    @Nullable
    public static View showGuideView(Activity activity, final View attachView, int layoutId, final boolean alignView,
                                     final boolean below, final int arrowId) {
        return showGuideView(activity, attachView, layoutId, alignView, below, arrowId, 0);
    }

    @Nullable
    public static View showGuideView(Activity activity, final View attachView, int layoutId, final boolean alignView,
                                     final boolean below, final int arrowId, final int yOffset) {
        return showGuideView(activity, attachView, layoutId, alignView, below, arrowId, yOffset, null);
    }

    @Nullable
    public static View showGuideView(Activity activity, final View attachView, int layoutId, final boolean alignView,
                                     final boolean below, final int arrowId, final int yOffset, final IResetLocationListener resetLocationListener) {
        return showGuideView(activity, attachView, layoutId, alignView, below, arrowId, yOffset, resetLocationListener, null);
    }

    @Nullable
    public static View showGuideView(Activity activity, final View attachView, int layoutId, final boolean alignView,
                                     final boolean below, final int arrowId, final int yOffset,
                                     final IResetLocationListener resetLocationListener, final GuideViewAnimator guideViewAnimator) {
        if (attachView == null || activity == null || activity.isFinishing()) {
            return null;
        }

        final View contentView = getContentView(activity);
        if (contentView instanceof FrameLayout) {
            FrameLayout parent = (FrameLayout) contentView;
            final View guideView = LayoutInflater.from(activity).inflate(layoutId, parent, false);
            if (guideViewAnimator != null) {
                guideView.setTag(R.id.guide_view_animator, guideViewAnimator);
            }
            parent.addView(guideView);
            guideView.setVisibility(View.INVISIBLE);
            guideView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    setGuideViewLocation(alignView, contentView, guideView, attachView, below, arrowId, yOffset, resetLocationListener);
                    if (guideView.getHeight() > 0) {
                        guideView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    }
                }
            });
            return guideView;
        }
        return null;
    }
    ....
}

使用kotlin的默认参数,我们可以少写很多重载方法

class GuideViewHelperFunc {

    companion object {
        fun showGuideView(
            activity: Activity?,
            attachView: View?,
            layoutId: Int,
            @IdRes arrowId: Int,
            alignView: Boolean = false,
            below: Boolean = false,
            yOffset: Int = 0,
            resetLocationListener: IResetLocationListener? = null,
            guideViewAnimator: GuideViewAnimator? = null
        ): View? {
            attachView?.let {
                activity?.let {
                    if (!it.isFinishing) {
                        val contentView = getContentView(activity)
                        if (contentView is FrameLayout) {
                            val parent = contentView as FrameLayout?
                            val guideView =
                                LayoutInflater.from(activity).inflate(layoutId, parent, false)
                            if (guideViewAnimator != null) {
                                guideView.setTag(R.id.guide_view_animator, guideViewAnimator)
                            }
                            parent!!.addView(guideView)
                            guideView.visibility = View.INVISIBLE
                            guideView.viewTreeObserver.addOnGlobalLayoutListener(object :
                                ViewTreeObserver.OnGlobalLayoutListener {
                                override fun onGlobalLayout() {
                                    setGuideViewLocation(
                                        alignView,
                                        contentView,
                                        guideView,
                                        attachView,
                                        below,
                                        arrowId,
                                        yOffset,
                                        resetLocationListener
                                    )
                                    if (guideView.height > 0) {
                                        guideView.viewTreeObserver.removeOnGlobalLayoutListener(this)
                                    }
                                }
                            })
                            return guideView
                        }
                    }
                }
            }
            return null
        }

        fun setGuideViewLocation(
            alignView: Boolean,
            contentView: View?,
            guideView: View,
            attachView: View?,
            below: Boolean,
            arrowId: Int,
            yOffset: Int,
            resetLocationListener: IResetLocationListener?
        ) {
        }

        fun getContentView(activity: Activity?): ViewGroup? {
            return activity?.findViewById(android.R.id.content)
        }
    }

使用的时候,我们可以使用必填的几个参数或者必传参数+选传参数,必要时,我们升值可以不按顺序写,这里还是推荐,按照顺序,并且赋值加上参数名称,类似第三种写法



 val layoutId = 0x22211
        val arrowId = 0x2223
        val viewGroup: ViewGroup = LinearLayout(this)

        //必传参数
        GuideViewHelperFunc.showGuideView(this, viewGroup, layoutId, arrowId)

        //必传参数+选传(按顺序)
        GuideViewHelperFunc.showGuideView(
            this,
            viewGroup,
            layoutId,
            arrowId,
            alignView = true,
            yOffset = 20
        )

        //必传参数+选传(顺序打乱)
        GuideViewHelperFunc.showGuideView(
            this,
            attachView = viewGroup,
            layoutId = layoutId,
            arrowId = arrowId,
            alignView = true,
            yOffset = 20
        )

        GuideViewHelperFunc.showGuideView(
            this,
            layoutId = layoutId,
            attachView = viewGroup,
            yOffset = 20,
            alignView = true,
            arrowId = arrowId
        )

6、各种集合

Kotlin标准库提供了基本类型的实现:Set、List以及Map。一对接口代表每种集合类型:

  • 只读接口:提供访问集合元素的操作【以listOf()创建的集合】
  • 可变接口:具有增删查改的功能的集合【以mutableListOf<String>()创建的集合】

集合图谱:


image
List

比如说我们来看一下List集合相关的内容,然后其实Set和Map都是差不多是如此设计的。

创建一个只读的List

val list1= listOf<String>()

然后你看list1的相关api,会发现竟然没有add函数???

image
image

再创建一个可变的List

val list2 = mutableListOf<String>()   

本质上其实是ArrayList

然后你看list2的相关api,这下正常多了吧,这才是Java中应该有的样子是吧。

可变集合其实就是Java中的List最原始的样子,而只读集合其实就是List最原始的样子去掉了增删改查的各个api。这样做,算是kotlin另辟蹊径。在某些情况下只读集合能保证多线程的稳定性。当然只读集合并不一定是真正的永不改变的,因为Kotlin设计的与Java的互操作性,而Java是没有只读集合的,那么有可能Kotlin传入Java的集合会是可变集合。

image
Set

set和list一样也是一对集合


val set1 = setOf<String>()
val set2 = mutableSetOf<String>()

其中,mutableSetOf本质上是LinkedHashSet。如果你想创建别的set集合。你可以看下这个,其实是和TreeSet,HashSet等等一一对应的

image
Map

Map和上面的List和set一样吧,有只读和可变的写法。另外更重要的是,map有些写法很特别

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

println("All keys: ${numbersMap.keys}")
println("All values: ${numbersMap.values}")
if ("key2" in numbersMap) println("Value by key \"key2\": ${numbersMap["key2"]}")    
if (1 in numbersMap.values) println("The value 1 is in the map")
if (numbersMap.containsValue(1)) println("The value 1 is in the map") // 同上

无论键值对的顺序如何,包含相同键值对的两个 Map 是相等的

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)    
val anotherMap = mapOf("key2" to 2, "key1" to 1, "key4" to 1, "key3" to 3)

println("The maps are equal: ${numbersMap == anotherMap}")

MutableMap 是一个具有写操作的 Map 接口,可以使用该接口添加一个新的键值对或更新给定键的值

val numbersMap = mutableMapOf("one" to 1, "two" to 2)
numbersMap.put("three", 3)
numbersMap["one"] = 11

println(numbersMap)

{one=11, two=2, three=3}

mutableMapOf的默认实现是LinkedHashMap

集合操作

Kotlin中最牛逼的我个人觉得就是跟Java Stream一样的东西,但是又没有版本兼容问题。

//Java 8 Android 7.0以上
 List<String> list = new ArrayList<>();
 list.stream().filter(s -> TextUtils.equals(s, "biubiu"));

Kotlin 无安卓版本限制
val list = mutableListOf<String>()
list.filter { TextUtils.equals(it, "biubiu") }

甚至可以一条龙操作

 list.filter { TextUtils.equals(it, "biubiu") }.map {
            //map操作
        }.takeWhile {
            //takeWhile操作
        }

再也不用写那么复杂的for循环去操作数据了。

惰性求值和序列

如果担心产生太多的集合,那可以用asSequence()和toList避免产生太多的中间list对象


list.asSequence().filter { TextUtils.equals(it, "biubiu") }.map {
            //map操作
        }.takeWhile {
            //takeWhile操作
        }.toList()

这个操作是先将集合变成序列,然后再这个序列上进行相应的操作,最后通过toList()转换为集合列表。实际使用过程中,只有调用了toList()【又叫做末端操作】,才会去真正的去求值。


【中间操作】
list.asSequence().filter { TextUtils.equals(it, "biubiu") }.map {
            //map操作
        }.takeWhile {
            //takeWhile操作
        }     

7、函数方法值类型

kotlin中的方法参数中声明的变量是final类型的,不能去改变的

image

如果非要修改,你需要

fun driveTrain(price: Int) {

        var priceTemp = price
        priceTemp = 33
        Log.e(TAG, "driverTrain一次的价钱:$priceTemp")
    }

这样设计有利有弊,某种程度上保证了传递进来的数据不被内部"污染",但是有时候可能会增加内存的使用。

8、高阶函数

Kotlin提供了不少高端的语法特性。比如let、with、run、apply、also等我们可以用它来改善项目中的一些写法。
比如let函数,定义一个变量在特定的作用域范围内生效,返回R值。

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

比如这个是Java中Adapter中常用的操作数据的逻辑

if (holder.musicSingerTv == null) {
  return;
}
holder.musicSingerTv.setText(entity.singer);
holder.musicSingerTv.setTextColor(Color.YELLOW);

如果换成kotlin,你可以这样

holder.musicSingerTv?.let {
            it.setText(entity.singer);
            it.setTextColor(Color.YELLOW);
        }

其他另外五种,可以去看下Kotlin系列之let、with、run、apply、also函数的使用

9、空安全

Kotlin中,变量分为可空类型和非空类型。

var str1: String? = null   可空类型
var str2: String = "ddd"   非空类型


str2 = null // 如果这样写,就会报错。要是在java中,会在运行时候报错

str1 = null // 如果可控类型这样写,就不会报错

对于可空类型,为了防止空指针,你可以使用安全调用操作符?.

比如

    fun driveTrain(train: Train?) {
        train?.openDoor() ?: print("train为空了")
    }

?:是Elvis操作符,上面的调用它的意思是说, train如果为空,那么不执行openDoor(),执行print("train为空了")

还有一个操作符是!!

    fun driveTrain(train: Train?) {
        train.openDoor()
    }

比如现在train是可空类型,但是你非要调用它的openDoor()方法,你会发现报错了Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Train

其实是觉得当前你的用法不安全。可以加上!!,变成这样,程序执行的时候有两种可能,要么正确返回name,要么抛出空指针异常。

 train!!.openDoor()

10、关于设计模式和Kotlin

工厂模式

常用于一个父类多个子类的时候,通过其来创建子类对象

比如说现在有一个汽车工厂,同时生产奥迪和五菱宏光,我们用熟悉的工厂模式来描述其业务逻辑如下:

interface ICar {
    val name: String
}

class AudiCar(override val name: String) : ICar {
    fun transportPig(count: Int) {
        print("运猪头数:$count")
    }
}

class SGMWCar(override val name: String) : ICar {
    fun transportPig(count: Int) {
        print("运火箭枚数:$count")
    }
}


class CarFactory {

    companion object {
        const val CAR_TYPE_AUDI = 0x001
        const val CAR_TYPE_SGMW = 0x002
    }

    fun produceCar(carType: Int): ICar? {
        return when (carType) {
            CAR_TYPE_AUDI -> AudiCar("奥迪")
            CAR_TYPE_SGMW -> SGMWCar("五菱")
            else -> null
        }
    }
}

fun main(args: Array<String>) {
    val audi = CarFactory().produceCar(CarFactory.CAR_TYPE_AUDI)
    audi?.transport(3)
}

这是简单的用kotlin模仿java的工厂模式,最后的创建工厂去生产车辆这里,kotlin中还可以更简化,因为kotlin天生支持单例,只需要将class改为object


object CarFactorySingleton {

    const val CAR_TYPE_AUDI = 0x001
    const val CAR_TYPE_SGMW = 0x002

    fun produceCar(carType: Int):ICar? {
        return when (carType) {
            CAR_TYPE_AUDI -> AudiCar("奥迪")
            CAR_TYPE_SGMW -> SGMWCar("五菱")
            else -> null
        }
    }
}

然后使用的时候可以变得很简洁


fun main(args: Array<String>) {
    val audi = CarFactorySingleton.produceCar(CarFactorySingleton.CAR_TYPE_AUDI)
    audi?.transport(33)
}

kotlin支持一种叫做operator操作符重载的功能【具体看操作符重载

比如上述的代码还可以修改为


object CarFactorySingletonOperator {

    const val CAR_TYPE_AUDI = 0x001
    const val CAR_TYPE_SGMW = 0x002

    operator fun invoke(carType: Int): Car? {
        return when (carType) {
            CAR_TYPE_AUDI -> AudiCar("奥迪")
            CAR_TYPE_SGMW -> SGMWCar("五菱")
            else -> null
        }
    }
}

调用的时候直接


    //运算符invoke
    val sgmw = CarFactorySingletonOperator(CarFactorySingletonOperator.CAR_TYPE_SGMW)
    sgmw?.transport(23)

可以看到变得更加简洁

这里的操作符的意思其实是这样的,比如CarFactorySingletonOperator() ----> CarFactorySingletonOperator.invoke(),所以上面重载运算符后可以直接变成后面那样调用了,跟直接创建一个类的实例没什么区别啦
image

更强大的,你还可以用上kotlin的伴生对象和操作符重载的特性去更加简洁的生成五菱宏光。
现在我们直接把生成的方法直接写到ICar中,如下


interface ICar {
    val name: String
    fun transport(count: Int)

    companion object {
        operator fun invoke(carType: Int): ICar? {
            return when (carType) {
                CarFactorySingletonOperator.CAR_TYPE_AUDI -> AudiCar("奥迪")
                CarFactorySingletonOperator.CAR_TYPE_SGMW -> SGMWCar("五菱")
                else -> null
            }
        }
    }
}


    //伴生对象直接生产工厂对象
    val sgmw = ICar(CarFactory.CAR_TYPE_SGMW)
    sgmw?.transport(3)

观察者和代理模式

观察者模式和代理模式是kotlin自身支持的特性,具体可以了解下委托属性的用法和实现。

委托属性

11、快速对比Java代码必备技能

Kotlin工具给我们提供了很好用的反编译工具,具体操作如下

  • 1、写一个kotlin版本的类和方法


    image
  • 2、点击Android Studio的Tools中的Show Kotlin ByteCode (装了Kotlin插件的Jertbain全家桶应该都是这样),这个时候你就能看到字节码了,

image
  • 3、点击字节码窗口的Decompile,字节码会转化为java代码

然后就是熟悉的Java代码了


image

三、参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容