Kotlin最近很火,我也认同Kotlin是一门经过深思熟虑的语言,除了下面提到的缺点。在本文中,我将分析我在开发过程中遇到的一些陷阱,并教你如何避免它们。null的奥秘在Kotlin中,你可以忘记如何在代码中处理null,这会让你忘记null无处不在,只是隐藏而已。看看这个表面上没问题的类:classFoo{privatevalc:Stringinit{bar()c=""}privatefunbar(){println(c.length)}}如果你尝试初始化这个类,代码会抛出一个NullPointerException异常.因为bar方法在初始化之前尝试访问c变量。虽然这段代码本身是有问题的,但是却导致了异常的抛出。但更糟糕的是您的编译器不会发现这一点。Kotlin在大多数情况下可以帮助你避免null,但你不能忘记null的存在。否则,迟早你会遇到类似的问题。null来自JDK的Kotlin标准库可以很好地处理null。但是如果你使用JDK中的类,你需要自己处理JDK方法调用可能出现的空指针。在大多数情况下,Kotlin的标准库就足够了,但有时您需要使用ConcurrentHashMap:valmap=ConcurrentHashMap()map["foo"]="bar"valbar:String=map["foo"]!!这时候,你需要使用!!操作员。但在某些情况下,您也可以将其替换为空安全运算符,例如(?)。但是,当您使用!!或者?,或者写一个适配器来使用Java类库,你会发现代码因为这些修改而变得凌乱。这是你无法回避的问题。您可能还会遇到许多更可怕的问题。当你在JDK类中使用方法时,返回值可能为null,并且没有像访问Map那样的语法糖。考虑以下示例:valqueue:Queue=LinkedList()queue.peek().toInt()在这种情况下,您使用可能返回空值的peek方法。但是Kotlin编译器不会提示你这个问题,所以当你的Queue为空时,可能会触发NullPointerException。问题是我们使用的Queue是JDK的一个接口,当你查看peek方法的文档时:或者{@codenull}ifthisqueueisempty*/Epeek();文档说peek方法返回一个E类型的对象,但Kotlin认为E是不可空的。下个版本的Kotlin可能会修复这个问题,但是目前在你的项目中使用类似的接口时,一定要注意:valqueue:Queue=LinkedList()queue.peek()?。toInt()inside当一个lambda表达式只有一个参数时,你可以在你的代码中省略它并使用它来代替。it:单个参数的内部名称。当你的表达式只有一个参数时,这是一个有用的特性,声明的过程可以省略(比如->),参数名就是它。问题是,当您的代码中有如下示例的嵌套函数时:vallist=listOf("foo.bar","baz.qux")list.forEach{it.split(".").forEach{println(it)}}it参数会被混淆。解决办法就是显示这样一条语句:list.forEach{item->item.split(".").forEach{part->println(part)}}是不是好看多了!隐藏复制下面注意类:dataclassFoo(valbars:MutableList)数据类提供了一系列的方法,你可以通过复制得到它的镜像。猜猜下面的代码会输出什么?valbars=mutableListOf("foobar","wombar")valfoo0=Foo(bars)valfoo1=foo0.copy()bars.add("oops")println(foo1.bars.joinToString())控制台会输出foobar,wombar,哎呀。问题在于复制方法实际上并没有复制一个完整的对象,而是对该对象的引用。当您忘记编写单元测试类并将数据类作为不可变类传递时,就会出现此问题。解决方法是在使用数据类的时候更加小心,当你必须把它作为一个值对象来使用时,像这样:dataclassFoo(valbars:List)数据类还有一个问题:它的equals/hashCode方法使用了属性是不可变的。您只能通过手动覆盖这些方法来修改返回值。请记住以上几点。内部方法暴露仔细考虑以下示例:classMyApi{funoperation0(){}internalfunhiddenOperation(){}}当您在Kotlin项目中引用此类时,internal关键字生效。但是当你从Java项目中使用它时,hiddenOperation变成了一个公共方法!为了避免这种情况,我建议使用一个接口来隐藏实现细节:interfaceMyApi{funoperation0()}classMyApiImpl:MyApi{overridefunoperation0(){}internalfunhiddenOperation(){}}specialglobalextension毫无疑问,扩展函数的功能非常重要的。但通常,能力越大,责任越大。例如,您可以编写全局JDK类扩展函数。但是当这个函数只在局部上下文中有意义,但是全局可见时,就会造成很多麻烦。funString.extractCustomerName():String{//...}跳转到您的方法的每个人都会不知所措。所以我认为在编写这样的方法之前三思而后行很重要。这里有一个建议:/***Returnsanelementofthis[List]wrappedinanOptional*whichisemptyif`idx`isoutofbounds.*/funList.getIfPresent(idx:Int)=if(idx>=size){Optional.empty()}else{Optional.of(get(idx))}/***Negates`isPresent`.*/funOptional.isNotPresent()=isPresent.not()lambdas单元返回值vsJavaSAM转换如果你的函数参数是lambdas表达式,返回值类型是Unit,可以省略return关键字:funconsumeText(text:String,fn:(String)->Unit){}//usageconsumeText("foo"){println(it)}这是一个非常有趣的特性,但是当你在Java代码中调用这个方法时会很别扭:consumeText("foo",(text)->{System.out.println(text);returnUnit。实例;});这对Java端不友好,如果想在Java中成功调用该方法,需要定义如下接口:interfaceStringConsumer{funconsume(text:String)}funconsumeText(text:String,fn:StringConsumer){}然后您可以使用Java的SAM转换。consumeText("foo",System.out::println);但是在Kotlin这边看起来很糟糕:consumeText("foo",object:StringConsumer{overridefunconsume(text:String){println(text)}})问题的关键点只有Java支持SAM转换,Kotlin不支持支持它。我的建议是针对简单的场景,使用Java的SAM接口作为消费者即可:("foo",System.out::println);在Java中使用不可变集合Kotlin提供了JDK集合类的不可变版本。funcreateSet():Set=setOf("foo")//...createSet().add("bar")//糟糕,编译错误这是一个很好的补充。但是当你查看JavaJDK的Set类API时,你会发现:createSet().add("bar");//UnsupportedOperationException当你试图修改这个Set时,会抛出这个异常,就像你使用一样作为Collections.unmodifiableSet()方法。我不知道是不是这样,但是在使用Kotlin的不可变版本的Java集合类时需要牢记这一点。接口中没有重载Kotlin不支持在接口上使用@JvmOverloads注解,当然override也不行。interfaceFoo{@JvmOverloads//OUCH!funbar(qux:String)}classFooImpl:Foo{@JvmOverloads//YIKES!overridefunbar(qux:String){}}只能这样手动定义:interfaceFoo{funbar()funbar(qux:String)}请记住,您可以使用Kotlin中的KEEP(KotlinEvolutionandEnhancementProcess)来改进。KEEP类似于Java中的JEP,但比JEP简洁得多。总结Kotlin现在很流行,我认为它是Java的增强版。但是在使用Kotlin时你仍然需要保持理智,尤其是当你正处于对Kotlin的各种炒作之中时。如果你打算使用Kotlin,一定要注意我们上面提到的与Kotlin相关的陷阱。***我还是想说,上面提到的问题都比较容易解决,不会对语言的使用造成实质性的伤害。