当前位置: 首页 > 科技观察

Java到Kotlin,以及Kotlin回到Java

时间:2023-03-21 01:10:52 科技观察

由于这篇博文引起了高度关注和争议,我们认为有必要添加一些关于我们如何在Allegro工作和做出决策的背景知识。Allegro拥有50多个开发团队,可以自由选择我们PaaS支持的技术。我们主要使用Java、Kotlin、Python和Golang编写代码。本文中提出的观点来自作者的经验。Kotlin很流行,Kotlin很时髦。Kotlin为您提供编译时空值安全和更少的样板代码。当然,它比Java好。你应该切换到Kotlin或作为一个老程序员死去。等等,或者你不应该?在开始使用Kotlin编写代码之前阅读项目的故事。关于技巧和障碍的故事变得如此烦人,以至于我们决定重写它。我们尝试过Kotlin,但现在我们正在用Java10重写我有我最喜欢的一组JVM语言。Java的/main和Groovy的/test对我来说是一个很好的组合。2017年夏天,我的团队启动了一个新的微服务项目,我们照例聊语言和技术。Allegro有几个支持Kotlin的团队,我们想尝试一些新的东西,所以我们决定尝试一下Kotlin。由于在Kotlin中没有Spock的替代品,我们决定继续在/test中使用Groovy(Spek不如Spock)。在2018年冬天每天与Kotlin相处几个月后,我们总结了利弊,得出的结论是Kotlin让我们的工作效率降低了。我们开始用Java重写这个微服务。这有几个原因:名称阴影类型推断编译时空指针安全类文字反向类型声明伴随对象集合文字也许吧?没有数据类公共类陡峭的学习曲线名字阴影这是Kotlin最让我惊讶的地方。看看这个函数:funinc(num:Int){valnum=2if(num>0){valnum=3}println("num:"+num)}当你调用inc(1)时它输出什么?在Kotlin的Method参数是一个值,所以你不能改变num参数。这是很好的语言设计,因为你不应该改变方法的参数。但是您可以定义另一个具有相同名称的变量并按照您想要的方式对其进行初始化。现在,在这个方法级范围内,您有两个名为num的变量。当然一次只能访问num中的一个,所以num的值会发生变化。一般,没有解决办法。在if的主体中,您可以添加另一个num,这并不令人震惊(新的块级范围)。好的,在Kotlin中,inc(1)输出2。但是在Java中,等效代码将无法编译。voidinc(intnum){intnum=2;//error:variable'num'isalreadydefinedinthescopeif(num>0){intnum=3;//error:variable'num'isalreadydefinedinthescope}System.out.println("num:"+num);}名称隐藏不是Kotlin发明的。这在编程语言中很常见。在Java中,我们习惯于使用方法参数隐藏类中的字段。publicclassShadow{intval;publicShadow(intval){this.val=val;}}阴影在Kotlin中有点矫枉过正。当然,这是Kotlin团队的设计缺陷。IDEA团队试图通过向您显示每个阴影变量并附上简洁的警告来解决此问题:Nameshadowed。两个团队都在同一家公司工作,所以也许他们可以互相交谈并就跟踪达成一致?我觉得——IDEA是对的。我无法想象隐藏方法参数的有效用例。类型推断在Kotlin中,当你声明一个var或val时,通常会让编译器根据右边表达式的类型来猜测变量类型。我们称之为局部变量类型推断,这对程序员来说是一个很大的进步。它允许我们在不影响静态类型检查的情况下简化代码。例如,这段Kotlin代码:vara="10"将被Kotlin编译器翻译成:vara:String="10",这在过去是相对于Java的真正优势。我是故意说是的,因为-好消息-Java10已经具有此功能,并且Java10现在可用。Java10中的类型涂层标头:vara="10";公平地说,我需要补充一点,Kotlin在这方面仍然略胜一筹。您还可以在其他上下文中使用类型推断,例如,单行方法。有关Java10中的局部变量类型推断的更多信息。编译时Null安全Null安全类型是Kotlin的杀手级功能。是个好主意。在Kotlin中,默认情况下类型是不可空的。如果需要可空类型,则需要添加?symbol,例如:vala:String?=null//okvalb:String=null//编译错误如果你使用一个可以为空的变量而没有进行空检查,那么Kotlin将无法编译,例如:println(a.length)//compilationerrorprintln(a?.length)//fine,printsnullprintln(a?.length?:0)//fine,prints0一旦你有了这两种类型,non-nullableT和nullableT?,你就可以忘记最常见的Java中的异常-NullPointerException。真的吗?不幸的是,事情并没有那么简单。当您的Kotlin代码必须与Java代码一起工作时,事情就变糟了(库是用Java编写的,所以我想这种情况经常发生)。然后,第三种类型跳了出来——T!叫做平台型,意思是T还是T?,准确点说是T!表示具有未定义空值的类型T。这种奇怪的类型无法在Kotlin中表示,只能从Java类型中推断出来。T!会误导你,因为它放宽了对null的限制并禁用了Kotlin的null安全限制。看看下面的Java方法:publicclassUtils{staticStringformat(Stringtext){returntext.isEmpty()?null:text;}}现在,您想从Kotlin调用format(string)。您应该使用哪种类型来使用此Java方法的结果?那么,你有三个选择。第一种方法。可以使用字符串,代码看起来很安全,但是会抛出空指针异常。fundoSth(text:String){valf:String=Utils.format(text)//compilesbutassignmentcanthrowNPEatruntimeprintln("f.len:"+f.length)}解决这个问题需要加判断:fundoSth(text:String){valf:String=Utils.format(text)?:""//println("f.len:"+f.length)}第二种方法。您可以使用String?,并且您的程序是null安全的。fundoSth(text:String){valf:String?=Utils.format(text)//safeprintln("f.len:"+f.length)//编译错误,fineprintln("f.len:"+f?.length)//null-safewith?operator}第三种方法。如果让Kotlin进行令人难以置信的局部变量类型推断会怎样?fundoSth(text:String){valf=Utils.format(text)//ftypeinferredasString!println("f.len:"+f.length)//compilesbutcanthrowNPEatruntime}坏主意。这段Kotlin代码看起来安全并且可以编译,但允许空值在您的代码中自由漫游,就像在Java中一样。还有一个技巧,!!操作员。使用这个强制推导f类型为String类型:fundoSth(text:String){valf=Utils.format(text)!!//throwsNPEwhenformat()returnsnullprintln("f.len:"+f.length)}中我的观点来吧,Kotlin类型系统中所有这些类似scala的东西!,?和!!,太复杂了。为什么Kotlin会从Java的T类型推断为T!而不是T??Java的互操作性似乎打破了Kotlin的杀手级特性——类型推断。看起来您应该显式声明类型(如T?)以满足由Java方法填充的所有Kotlin变量。类文字类文字在使用Log4j或Gson等Java库时很常见。在Java中,我们写的类名带有.class后缀:Gsongson=newGsonBuilder().registerTypeAdapter(LocalDate.class,newLocalDateAdapter()).create();在Groovy中,类文字被简化为必需品。您可以省略.class,它是Groovy类还是Java类都没有关系。defgson=newGsonBuilder().registerTypeAdapter(LocalDate,newLocalDateAdapter()).create()Kotlin区分了Kotlin类和Java类,并为它们准备了不同的语法形式:valkotlinClass:KClass=LocalDate::classvaljavaClass:Class=LocalDate::class.java所以在Kotlin中,你必须写:valgson=Gs??onBuilder().registerTypeAdapter(LocalDate::class.java,LocalDateAdapter()).create()这真的很难看。逆序类型声明在C系列编程语言中,有一种声明类型的标准方法。即先写出类型,再写出声明为该类型的东西(变量、字段、方法等)。在Java中表示如下:intinc(inti){returni+1;}在Kotlin中,它以相反的顺序表示:funinc(i:Int):Int{returni+1}这很烦人,因为:首先,你不得不写或者阅读名称和类型之间那个讨厌的冒号。这个额外的字母到底是做什么的?为什么名称与类型分开?我不知道。但我知道这会让使用Kotlin变得更加困难。第二个问题。阅读方法声明时,最不想知道的就是方法名和返回类型,然后才是参数。在Kotlin中,方法的返回类型远在行尾,因此您可能需要滚动阅读:privatefungetMetricValue(kafkaTemplate:KafkaTemplate,metricName:String):Double{...}Anothercase,如果参数是用分支的格式写的,就得找返回类型。在下面的方法定义中找到返回类型需要多长时间?@BeanfunkafkaTemplate(@Value("\${interactions.kafka.bootstrap-servers-dc1}")bootstrapServersDc1:String,@Value("\${interactions.kafka.bootstrap-servers-dc2}")bootstrapServersDc2:String,cloudMetadata:CloudMetadata,@Value("\${interactions.kafka.batch-size}")batchSize:Int,@Value("\${interactions.kafka.linger-ms}")lingerMs:Int,metricRegistry:MetricRegistry):KafkaTemplate{valbootstrapServer=if(cloudMetadata.datacenter="dc1"){bootstrapServersDc1}...}倒序第三个问题是它限制了IDE的自动补全。在标准顺序中,很容易找到类型,因为它以类型开头。一旦确定了类型,IDE就可以根据类型建议一些与其相关的变量名。这允许快速输入变量名,不像这样:MongoExperimentsRepositoryrepository即使在像Intellij这样好的IDE中,为Kotlin输入这样的变量名也不容易。如果代码中有很多Repositories,在自动补全列表中很难找到匹配的。换句话说,您必须手动输入完整的变量名称。repository:MongoExperimentsRepository伴侣对象,供即将加入Kotlin阵营的Java程序员使用。“嗨Kotlin。我是新来的,有可用的静态成员吗???”他问。“不。我是面向对象的,静态成员不是,”Kotlin回答道。“好的,但是我需要一个MyClass的记录器,我该怎么办?”“当然,你可以使用伴生对象。”“伴生对象是什么鬼?”“它是一个绑定到类的单例对象。您可以将记录器放在伴随对象中,”Kotlin解释道。“明白了,是吗?”classMyClass{companionobject{vallogger=LoggerFactory.getLogger(MyClass::class.java)}}“是的!”,现在我可以像这样调用日志记录——MyClass.logger——就像在Java中使用静态成员一样吗?它只是一个对象。想象一下,这是一个匿名内部类的单例实现。其实这个类不是匿名的,它叫Companion,名字可以省略。看?它非常简单。”我喜欢对象声明的概念——单例是一种非常有用的模式。从语言中删除静态成员是不现实的。我们在Java中使用静态记录器已有好几年了,它是一个很经典的图案。由于它只是一个记录器,我们不关心它是否是纯OO的。只要它有效并且不会造成损坏。有时,我们必须使用静态成员。好的旧publicstaticvoidmain()仍然是启动Java应用程序的唯一方法。试图在没有Google帮助的情况下编写这个伴随对象。classAppRunner{companionobject{@JvmStaticfunmain(args:Array){SpringApplication.run(AppRunner::class.java,*args)}}}集合字面量初始化列表在Java中需要大量模板代码:importjava.util。数组;...Liststrings=Arrays.asList("Saab","Volvo");初始化Map比较麻烦,所以很多人用Guava:importcom.google.common.collect.ImmutableMap;...Mapstring=ImmutableMap.of("firstName","John","lastName","母鹿");我们还在等待Java产生新的语法来简化集合和映射表的文字表达。这种语法在许多语言中都很自然和方便。JavaScript:constlist=['Saab','Volvo']constmap={'firstName':'John','lastName':'Doe'}Python:list=['Saab','Volvo']map={'firstName'':'John','lastName':'Doe'}Groovy:deflist=['Saab','Volvo']defmap=['firstName':'John','lastName':'Doe']简单地说,简洁的集合文字语法在现代编程语言中备受期待,尤其是在初始化集合时。Kotlin提供了一系列内置函数来替换集合字面量:listOf()、mutableListOf()、mapOf()、hashMapOf()等。Kotlin:vallist=listOf("Saab","Volvo")valmap=mapOf("firstName"to"John","lastName"to"Doe")映射表中的键和值通过to操作符关联在一起,这很好,但为什么不使用熟悉的冒号(:)呢?多么令人失望!或许?没有函数式编程语言(比如Haskell)没有null。他们提供MaybeMonads(如果您不了解Monads,请阅读TomaszNurkiewicz的这篇文章)。很久以前,Scala将Maybe作为Option引入了JVM世界,随后在Java8中被采用,成为了Optional。Optional现在广泛用于API边界,以处理可能包含空值的返回类型。在Kotlin中没有等同于Optional的东西。看起来您应该使用Kotlin的可空类型包装器。让我们研究一下这个问题。通常,在使用Optional时,首先会进行一系列的null-safe转换,最后处理null值。例如,在Java中:publicintparseAndInc(Stringnumber){returnOptional.ofNullable(number).map(Integer::parseInt).map(it->it+1).orElse(0);}在Kotlin中也很好,使用let函数:funparseAndInc(number:String?):Int{returnnumber.let{Integer.parseInt(it)}.let{it->it+1}?:0}OK?是的,但没那么简单。上面的代码可能会出错,从parseInt()中抛出NPE。Monad风格的map()只能在值存在时执行,否则,null将被简单地传递。这就是map()方便的原因。不幸的是,Kotlin的let不是那样工作的。它只是简单地从左到右执行调用,它不关心它是否为空。因此,要使此代码为null安全,您必须添加?beforelet:funparseAndInc(number:String?):Int{returnnumber?.let{Integer.parseInt(it)}?.let{it->it+1}?:0}现在举个例子,两个版本的可读性Java和Kotlin,你更喜欢哪一个?