从Java的角度看kotlin特性(二)

8 篇文章 0 订阅
6 篇文章 0 订阅

之前有从基本语法和用法的角度聊过kotlin与java的关系,如果不熟悉可以参照:从java的角度看kotlin特性(一)

引言

从语法的角度来看,kotlin像是java的升级与增强,事实上,随着java版本的提高,现代语言的多种特性也被加入其中,比如:

  • java8中的lambda表达式,实现函数式编程
  • java9中类似类似nodejs的模块化系统,类似node命令行的jshell
  • java10中类似弱类型语言的局部变量类型判断

在2017年谷歌宣布kotlin成为安卓官方开发语言后,这门语言才算真正进入人们的视野,它具备了现代语言所有的许多特性,简单方便,在不考虑性能的情况下,确实是一门很好的语言.
当然,里面很多语法与编码形式与java不同,可能会致使熟悉java的人在使用kotlin时初始期间速度有所不足.

不过总的来说,经kotlin改写的代码量会大幅度减少,因此学习一下还是很有必要的,这里接着上一节在语法和使用层面看一下kotlin的一些特性

静态方法与静态属性

在java中经常会使用到静态方法与静态属性,方便操作,不需要实例化对象(class类加载不予考虑),像这样:

public class JTest {
    public static final String a = "aaa";
    public static String b = "bb";
    public static void f1() {
    }
}

在java中定义了静态方法,静态常量和静态属性,现在通过工具转成kotlin代码:

object JTest {
    val a = "aaa"
    var b = "bb"
    fun f1() {
    }
}

在kotlin中,是没有static概念的,习惯了java的人需要转变一下思维;

将这段kotlin代码通过反编译形成java进行查看:

public final class JTest {
   @NotNull
   private static final String a = "aaa";
   @NotNull
   private static String b;
   public static final JTest INSTANCE;

   @NotNull
   public final String getA() {
      return a;
   }

   @NotNull
   public final String getB() {
      return b;
   }

   public final void setB(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      b = var1;
   }

   public final void f1() {
   }

   static {
      JTest var0 = new JTest();
      INSTANCE = var0;
      a = "aaa";
      b = "bb";
   }
}

可以看到kotlin中的单例object实际上使用的是静态域,这和java中的单例是完全不同的
在java中,单例对象和一般的实例没有任何区别,字段同样会存储在虚拟机堆中;
而kotlin中这种单例方式,字段与class类关联

我们不能说哪种方式比较好,从访问权限上来说,两者没有区别,不过因为kotlin中静态域的使用,可以防止反射对单例的攻击

而java中原本的静态方法f1,在经过一系列转换为,成了一个final类型的成员函数,这点也需要考虑到

通过查看kotiln转成的java代码可以发现,这些静态属性访问类型都是private,java对静态属性的访问会转变为kotlin中实例对成员方法的访问.

如果现在需要模仿java,将kotlin代码中这些字段转变为public访问属性,该怎么处理呢?
可以这样:

object JTest {
    const val a = "aaa"

    var b = "bb"

    fun f1() {

    }
}

val类型字段上添加 const,可以让该字段变为public访问类型;需要注意的是,const字段是无法使用在var声明的字段上的

const字段的含义可以参考:Kotlin中const修饰符详解

或者可以这样:

object JTest {
    @JvmField
    val a = "aaa"

    @JvmField
    var b = "bb"

    fun f1() {

    }
}

@JvmField编注表示字段采用jvm原有的声明方式,不会将该字段转变为get***和set***方法

@JvmStatic@JvmField功能相似,不过作用于方法,可以将一般的方法转为静态方法

那么,使用时不禁会想,kotlin中有那些方法可以声明静态属性呢?

1. Object类中声明

从前面的代码就可以看到,在object中声明属性,实际上就是静态的,再加上const 和 @JvmField来控制访问权限,基本可以做到和java中的静态属性相同

2. 伴生类中声明

kotlin为了弥补静态属性的不足,提出了伴生类的概念:companion object

class AAA {
    var m: String = ""

    companion object {
        val a = 1
        var b = "1"
        const val c = 1
        val d = intArrayOf(1)
    }
}

比如上面的类:AAA 对应的java文件大概是这样:

public final class AAA {
   @NotNull
   private String m = "";
   private static final int a = 1;
   @NotNull
   private static String b = "1";
   public static final int c = 1;
   @NotNull
   private static final int[] d = new int[]{1};
   public static final AAA.Companion Companion = new AAA.Companion((DefaultConstructorMarker)null);

   @NotNull
   public final String getM() {
      return this.m;
   }

   public final void setM(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.m = var1;
   }

   public static final class Companion {
      public final int getA() {
         return AAA.a;
      }

      @NotNull
      public final String getB() {
         return AAA.b;
      }

      public final void setB(@NotNull String var1) {
         Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
         AAA.b = var1;
      }

      @NotNull
      public final int[] getD() {
         return AAA.d;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

可以看到,伴生类的成员被当做静态变量或静态常量

使用Const的区别只是在于访问属性的不同

3. 文件级声明

kotlin和java有一点不同,kotlin不强制类名和文件名相同(java中必定会有一个类与文件名相同),在同一个kotlin文件中,可以同时存在多个顶级类(可以同时为public访问类型)

比如这样:

这里写图片描述

在kotlin文件中可以直接声明方法以及成员:

这里写图片描述

对应的java代码大概为:

public final class TestKt {
   private static final int a = 1;
   private static final int b = 2;
   public static final int c = 3;
   private static final int d = 4;
   @NotNull
   private static final int[] e = new int[]{1};

   public static final void main(@NotNull String... a) {
      Intrinsics.checkParameterIsNotNull(a, "a");
   }

   public static final int getB() {
      return b;
   }

   @NotNull
   public static final int[] getE() {
      return e;
   }
}

访问器属性

java中访问器属性分为四种:

  1. 全局开放:public
  2. 继承访问:protected
  3. 包级策略:default
  4. 私有权限:private

概念比较清晰,基本上不会有什么混乱发生.

kotlin中对访问器属性的概念显的有些“敷衍”,至少对于成员域来说,默认都会将其变为 private 访问级别,然后通过添加get和set方法来控制读取和赋值.

我们无法说明两者的优劣性,因为kotlin更侧重于代码的简洁以及空指针的控制,只有在使用的时候,尽量保证访问属性合理就好.

赋值操作的能力

在java中,经常会使用这样的代码:

public class JTest {
    public static void main(String[] args) {
        String name = "1";
        set(name = "2");
    }

    private static void set(String name) {

    }
}

同样的,如果使用kotlin,不自觉就会这样:

fun main(vararg a: String) {
    var name = "1"
    set(name = "2")
}

fun set(name: String) {
}

……
……

执行完以后就会发现,name结果大不相同.

在java中我们习惯这样来处理变量:

int a, b, c;
a = b = c = 1;

可以连续赋值,在kotlin中这样就会很麻烦了.

并非kotlin不想实现类似java的功能,而是有深层的原因,像这样:

这里写图片描述

可见,kotlin中,虽然赋值方式并非一个表达式,没有结果值,可能开始有些不习惯.

但是!: 赋值操作有颜色啊,细心点也不会弄错了.

所以,见到这样的提示就不需要好奇了:

这里写图片描述

注解继承

java中会通过注解来标注类或者字段等的特性,有些只是起到警示作用,有些这可以保留到运行时.比如可以自定义注解,将安卓中一般都会使用的两个xml文件放到注解上:

这里写图片描述

这里写图片描述

这样对于一般activity的来说,可以直接查找到menu,layout文件.

这里定义注解时,可以看到有一个类为:@Inherited,表示注解继承;
即父类拥有的注解,子类同样会拥有.比如这样:

这里写图片描述

但是如果这样:

这里写图片描述

所以,在使用java原有的一些特性时,还是直接使用:

**.javaClass 或者 **::class.java

自定义全局方法

kotlin中可以动态的添加方法.

比如,在安卓中最经常的会发送全局toast,那么可以这样:

/**
 * 显示toast,可以返回自身
 */
@Suppress("NOTHING_TO_INLINE")
inline fun <T> T.toast(msg: String?): T {
    RxBus.instance?.post(BaseEvent(Const.SHOW_TOAST, msg ?: ""))
    return this
}

这样在使用时就可以随时随地toast:

class AAA{
    fun f1(): Unit {
        toast("msg")   
    }
}

还可以这样:

class AAA{
    fun f1(): Int {
        return 1.toast("msg")
    }
}

甚至于可以这样

class AAA {
    fun f1(b: Boolean): Unit {
        if (b) return Unit.toast("msg")

        // ...
    }
}

在kotlin中,高级函数使用起来非常方便,我们可以模仿高级函数把toast改写一下:

/**
 * 显示toast,可以返回最后一行
 */
inline fun <T, R> R.let_toast(msg: String?, block: (R) -> T): T {
    RxBus.instance?.post(BaseEvent(Const.SHOW_TOAST, msg ?: ""))
    return block(this)
}

/**
 * 显示toast,可以返回自身
 */
inline fun <R> R.also_toast(msg: String?, block: (R) -> Unit): R {
    RxBus.instance?.post(BaseEvent(Const.SHOW_TOAST, msg ?: ""))
    block(this)
    return this
}

/**
 * 显示toast,可以返回最后一行
 */
inline fun <T, R> R.run_toast(msg: String?, block: R.() -> T): T {
    RxBus.instance?.post(BaseEvent(Const.SHOW_TOAST, msg ?: ""))
    return block(this)
}

/**
 * 显示toast,可以返回自身
 */
inline fun <R> R.apply_toast(msg: String?, block: R.() -> Unit): R {
    RxBus.instance?.post(BaseEvent(Const.SHOW_TOAST, msg ?: ""))
    block(this)
    return this
}

这些”小点子“并不能做到性能优化,但是在编写代码时,会非常的方便:比如我们经常使用的view的点击事件绑定:

/**
 * 添加onClick防抖动监听
 */
fun <T : View> T.dOnClick(action: T.() -> Unit) {
    this.setOnClickListener(object : DebouncingOnClickListener() {
        override fun doClick(v: View?) {
            action(this@dOnClick)
        }
    })
}

/**
 * 添加onClick防抖动监听,[action]为需要执行的代码
 *
 * 如果[filter]返回true,则执行注册的事件,否则,不执行
 */
fun <T : View> T.dOnClick(filter: () -> Boolean, action: T.() -> Unit) {
    this.setOnClickListener(object : DebouncingOnClickListener() {
        override fun doClick(v: View) {
            if (filter()) action(this@dOnClick)
        }
    })
}

这样,在活动或者其他地方就可以直接函数式调用并达到防止抖动的效果:

//如果verity_random方法返回了true,才会执行函数体中的内容
bt_common_function.dOnClick(::verity_random) {
    //逻辑操作
}

类似的还有很多,自要有想法都可以进行模版抽取

那么这样的功能,是如何实现的呢:

public static final void dOnClick(@NotNull final View $receiver, @NotNull final Function0 filter, @NotNull final Function1 action) {
   Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
   Intrinsics.checkParameterIsNotNull(filter, "filter");
   Intrinsics.checkParameterIsNotNull(action, "action");
   $receiver.setOnClickListener((OnClickListener)(new DebouncingOnClickListener() {
      public void doClick(@NotNull View v) {
         Intrinsics.checkParameterIsNotNull(v, "v");
         if ((Boolean)filter.invoke()) {
            action.invoke($receiver);
         }

      }
   }));
}

很明显的,其实是生成了静态方法,然后将this作为方法的第一参数传入,来达到 动态添加函数 的效果.

至于函数体,其实对应了类:Function**

when和if的返回值

安卓平台上,kotlin中使用最多的除了“kotlin-android-extensions”提供了自动绑定id,可能就是when和if了.

对于很多输入框的界面,进行最后功能操作时,需要验证很多内容:

比如进行注册时,需要验证密码等信息:

private boolean verity_register() {
    if ((registerType = verity_account()) == -1) {
        return false;
    }
    randomCode = mEtRandomCode.getText().toString();
    passwordOne = mEtPasswordOne.getText().toString();
    passwordTwo = mEtPasswordTwo.getText().toString();
    invitationCode = mEtInvitationCode.getText().toString();
    if (TextUtils.isEmpty(randomCode)) {
        showToast(String.format(Locale.CHINA, "请输入%s验证码", registerType == 1 ? "短信" : "邮箱"));
        return false;
    }
    if (!randomCode.matches(RegexConst.REGEX_RANDOM_NUMBER_6)) {
        showToast(String.format(Locale.CHINA, "%s验证码格式错误", registerType == 1 ? "短信" : "邮箱"));
        return false;
    }
    if (TextUtils.isEmpty(passwordOne)) {
        showToast("请输入登录密码");
        return false;
    }
    if (TextUtils.isEmpty(passwordTwo)) {
        showToast("请确认登录密码");
        return false;
    }
    if (!passwordOne.equals(passwordTwo)) {
        showToast("两次输入的密码不同");
        return false;
    }
    if (!passwordOne.matches(RegexConst.REGEX_PASSWORD)) {
        showToast("密码须为6-16位字母数字组合");
        return false;
    }
    return true;
}

虽然很整齐,但略显啰嗦,但使用kotlin则会方便很多,另外结合之前提到的toast函数,可以简写成如下方式:

private fun verity_register(): Boolean {
    randomCode = et_random_code.text.toString()
    passwordOne = et_password_one.text.toString()
    passwordTwo = et_password_two.text.toString()

    return verity_account().run {
        when {
            this == -1 -> false
            randomCode.isEmpty() -> false.toast("请输入${if (registerType == 1) "短信" else "邮箱"}验证码")
            !randomCode.matches(RegexConst.REGEX_RANDOM_NUMBER_6.toRegex()) -> false.toast("${if (registerType == 1) "短信" else "邮箱"}验证码格式错误")
            passwordOne.isEmpty() -> false.toast("请输入登录密码")
            passwordTwo.isEmpty() -> false.toast("请确认登录密码")
            passwordOne != passwordTwo -> false.toast("两次输入的密码不同")
            !passwordOne.matches(RegexConst.REGEX_PASSWORD.toRegex()) -> false.toast("密码须为8-16位字母数字组合")
            else -> true.toast("信息验证成功")
        }
    }
}

if表达式同样如此

这里还可以结合全局函数来使用:

/**
 * 专门重写Boolean方法,folse与ture时执行不同的lambda,返回相同的值
 */
inline fun <T> Boolean.kindAnyReturn(onFalse: () -> T, onTrue: () -> T): T {
    return if (this) onTrue() else onFalse()
}

/**
 * 专门重写Boolean方法,folse与ture时执行不同的lambda,返回不同的值
 */
inline fun Boolean.kindUnitReturn(onFalse: () -> Unit, onTrue: () -> Unit) {
    if (this) onTrue() else onFalse()
}

使用时可以避免if和else的相互嵌套:

fun f1(b: Boolean): Unit {
    val num = b.kindAnyReturn({ 1.toast("false") }) {
        toast("true")
        2
    }
}

强大的流操作

java8中提供了流处理功能,可以参考:java8–stream

kotlin中提供的功能则更加强大,且由于空类型预处理的存在,可以不用使用Optional

例如现在为了实现一个List队列,元素为Map,就可以这样处理:

val list = mutableListOf<Map<String, Any?>>()
dataBean.forEach {
    list.add(mapOf(
            "name" to it.name,
            "age" to it.age,
    ))
}

继续查看forEach的实现方式:

/**
 * Performs the given [action] on each element.
 */
@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

可以看到真实的代码和我们一般想法相同,可以查看对应的java文件:

   public static final void forEach(@NotNull Iterable $receiver, @NotNull Function1 action) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      Intrinsics.checkParameterIsNotNull(action, "action");
      Iterator var4 = $receiver.iterator();

      while(var4.hasNext()) {
         Object element = var4.next();
         action.invoke(element);
      }

   }

lambda的精髓在于:在调用表达式时,会直接将代码内联.

fun main(vararg args: String) {
    args.forEach {
        print("it")
    }
}

在生成字节码时,并不会去调用forEach静态方法,而是直接将代码嵌入:

public static final void main(@NotNull String... args) {
   Intrinsics.checkParameterIsNotNull(args, "args");
   Object[] $receiver$iv = (Object[])args;
   int var2 = $receiver$iv.length;

   for(int var3 = 0; var3 < var2; ++var3) {
      Object element$iv = $receiver$iv[var3];
      String it = (String)element$iv;
      String var6 = "it";
      System.out.print(var6);
   }

}

java->kotlin类型检测

java中,一个对象是否为空只能通过主动判断,在kotlin中则是强行指定的,则会导致在java向kotlin转换时,如果不注意控制检测,代码会主动崩溃:

现在有个java类:

public abstract class JTest {
    abstract void f(String a);
}

包含抽象方式,子类在继承时,需要重写该方法:

class AAA : JTest() {
    override fun f(a: String) {
        println("error")
    }
}

此时,编译器并不会提示任何异常,代码中也没有调用变量a,按说即便调用方法a时传入null,也不会有任何问题.

但在真正调用的时候,会发现在传入null的情况下方法会空指针异常.

并且这种问题比较常见,项目中很多地方调用的api都是java编写的.

之前也有说过:kotlin对于空指针的控制是通过异常来实现的,因此,并不是说使用kotlin就可以完全避免空指针.

同样的问题还存在于反射赋值中:

java进行反射赋值时,可以绕过泛型等检测;kotlin赖以生存的赋值时检测会因为反射的存在,而出现漏洞

安卓中请求网络时一般会使用Retrofit+RxJava+Gson模版,其中在处理网络返回数据时,版本会利用Gson库将json类型数据转换成Bean类.

这里的 转换 是通过反射完成的,因此即便我们在类中这样指定:

var sales: MutableList<GoodsCollectionFootBean> = mutableListOf()

在后台数据的sales字段返回null时,字段依然会被Gson库赋值为null.

类似这样的地方肯定不少,因此在写代码时,必须考虑数据的来源以及可能的赋值方式.

好在如果只是Gson库的话,我们可以通过重写Gson库的转换方式来达到非Null效果

public final class ListTypeAdapterFactory implements TypeAdapterFactory {

    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        Type type = typeToken.getType();

        Class<? super T> rawType = typeToken.getRawType();
        if (!List.class.isAssignableFrom(rawType)) {
            return null;
        }

        Type elementType = $Gson$Types.getCollectionElementType(type, rawType);
        TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));

        @SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter
                TypeAdapter<T> result = new Adapter(gson, elementType, elementTypeAdapter);
        return result;
    }

    private static final class Adapter<E> extends TypeAdapter<List<E>> {
        private final TypeAdapter<E> elementTypeAdapter;

        public Adapter(Gson context, Type elementType,
                       TypeAdapter<E> elementTypeAdapter) {
            this.elementTypeAdapter = new TypeAdapterRuntimeTypeWrapper<E>(
                    context, elementTypeAdapter, elementType);
        }

        //关键部分是这里,重写解析方法
        public List<E> read(JsonReader in) throws IOException {
            //新建一个空的列表
            List<E> list = new LinkedList<>();

            //null值返回null
            if (in.peek() == JsonToken.NULL) {
                in.nextNull();
                return list;
            }
            try {
                in.beginArray();
                while (in.hasNext()) {
                    E instance = elementTypeAdapter.read(in);
                    list.add(instance);
                }
                in.endArray();
                //正常解析成为列表
            } catch (IllegalStateException e) { //如果是空字符串,会有BEGIN_ARRAY报错

            }
            return list;
        }

        public void write(JsonWriter out, List<E> list) throws IOException {
            if (list == null) {
                out.nullValue();
                return;
            }

            out.beginArray();
            for (E element : list) {
                elementTypeAdapter.write(out, element);
            }
            out.endArray();
        }
    }
}

通过注册转换器,在处理数组对象时,将null解析为new LinkedList<>()

然后在创建Gson实例时注入即可:

val gson = GsonBuilder()
                .serializeNulls()
                .registerTypeAdapterFactory(ListTypeAdapterFactory())//对空列表处理
                .setDateFormat("yyyy:MM:dd HH:mm:ss")
                .create()

变量名遮盖

kotlin中提供了类似JS的with功能,这使得批量成员赋值时特别方便:

比如结合之前添加的dOnclick方法,很少的代码便可以完成文字赋值与事件绑定功能:

bt_common_function.run {
    text = "重置密码"
    dOnClick {
        finish()
    }
}

且由于变量名查找机制的不同,我们也不需要担心性能和作用域的问题:
js中为什么不提倡使用with函数

与之对比,kotlin中with寻址方式则有些不同:

1. 首先查找局部变量表
2. 查找with指定的对象字段
3. 父类中指定的字段
4. 其他字段(比如静态字段等)

查找时会先查找动态成员域,再选择静态成员域

这时,会出现这样的情况

class AAA {
    var size = 1
}

class BBB {
    var size=1

    fun main(vararg args: String) {
        with(AAA()) {
            size = 2
        }
    }
}

在不清楚类AAA具体的成员域之前,由于size字段被遮盖,导致类BBB的成员域size没有任何改变,结果自然会出现不可预料的错误.

且这种错误很难检查到,毕竟单从代码上来说,逻辑上没有任何问题.此时必须指定size所属的对象:

with(AAA()) {
    this@BBB.size = 2
}

最后

可以看到,虽然说kotlin是一种可以与java互通的jvm语言,但毕竟侧重的方向不同,因此在使用时需要考虑一些额外的因素.

在泛型,继承,访问属性,等方面,java做的比较好,可以保证代码的严谨性.

而在逻辑代码的编写等方面,因为kotlin大量类似 “语法糖” 的存在,会方便的多.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值