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

Java简洁之道

时间:2023-03-13 00:20:17 科技观察

计算机专家在解决问题时非常看重表达简洁的价值。Unix先驱KenThompson曾经说过一句非常有名的话:“我丢弃1000行代码的那一天是我最富有成效的日子之一”。对于需要持续支持和维护的任何软件项目来说都是如此。是当之无愧的目标。早期的Lisp贡献者PaulGraham甚至将这种语言的简单性等同于它的强大功能。这种对能力的认识使得能够编写紧凑、简洁的代码成为许多现代软件项目选择语言的首要标准。任何程序都可以通过重构来缩短,去掉冗余代码或无用的占位符,比如空格,但有些语言天生就具有表达能力,特别适合编写短小的程序。意识到这一点,Perl程序员普及了代码高尔夫比赛。目标是用尽可能短的代码解决一个特定的问题或实现一个给定的算法。APL语言的设计理念是使用特殊的图形符号,让程序员用少量的代码写出功能强大的程序。这样的程序,如果实施得当,可以很好地映射到标准的数学表达式。简洁的语言在快速创建小脚本方面非常有效,尤其是在干净且定义明确的问题域中,其目的不会因简洁而黯然失色。与其他编程语言相比,Java语言以冗长着称。这样做的主要原因是程序开发社区制定的惯例,在许多情况下,任务的完成会更多地考虑描述性和控制性。例如,从长远来看,长变量名使大型代码库更具可读性和可维护性。描述性的类名通常映射到文件名,在向现有系统添加新功能时使其一目了然。如果你能坚持下去,一个描述性的名字可以极大地简化在你的应用程序中指示特定功能的文本搜索。这些实践使得Java在大型复杂代码库的大规模实现中极为成功。对于小型项目来说,简单是受欢迎的,某些语言非常适合在命令提示符下编写简短的脚本或交互式探索编程。Java作为一种通用语言,更适合编写跨平台工具。在这种情况下,使用“冗长的Java”并不一定会带来额外的价值。虽然编码风格可以在变量命名等方面发生变化,但从历史上看,Java语言在某些基本层面上仍然需要比其他语言更多的字符来完成相同的任务。为了应对这些限制,Java语言一直在不断更新以包含通常称为“语法糖”的功能。使用这些习语可以实现用更少的字符表示相同功能的目标。这些习语比它们更冗长的对应物更受编程社区的欢迎,并且通常很快被社区采纳为常用用法。本文将重点介绍编写干净Java代码的最佳实践,尤其是与JDK8中的新功能有关的最佳实践。简而言之,Java8中Lambda表达式的引入让更优雅的代码成为可能。这在使用新的JavaStreamingAPI处理集合时尤为明显。冗长的JavaJava代码冗长的名声部分是由于其面向对象的实现风格。在许多语言中,经典的“HelloWorld”程序示例可以用不超过20个字符的单行代码实现。在Java中,除了类定义中包含的main方法外,main方法还需要包含一个方法调用,通过System.out.println()将字符串打印到终端。即使在使用最少的方法限定符、括号和分号并删除所有空格的极端情况下,“HelloWorld”程序也至少需要86个字符。添加空格和缩进以提高可读性,不用说,“HelloWorld”程序的Java版本给人的第一印象是冗长。Java代码冗长的部分原因还在于Java社区将描述性而非简洁性作为标准。在这一点上,选择与代码格式化美学相关的不同标准是无关紧要的。此外,样板代码的方法和部分可以包含在集成到API中的方法中。为简单起见的程序代码重构可以在不牺牲准确性或清晰度的情况下极大地简化冗余的Java代码。在某些情况下,Java冗长的名声是由于大量的旧代码示例造成的错觉。许多关于Java的书籍都是很多年前写的。由于Java在整个万维网首次出现时就已经存在,因此许多Java在线资源都提供了Java语言最早版本的代码片段。随着时间的推移,一些可见的问题和不足得到了改善,Java语言已经成熟,以至于即使是非常准确和很好实现的案例也不一定能有效地使用后来的语言习语和API。Java的设计目标包括面向对象、易用性(当时,这意味着使用C++风格的语法)、健壮性、安全性、可移植性、多线程和高性能。简单不是其中之一。函数式语言为以面向对象语法实现的任务提供了更简洁的替代方案。Java8中添加的Lambda表达式改变了Java的表示方式,减少了执行许多常见任务所需的代码量,并为Java的函数式编程习语打开了大门。函数式编程函数式编程使用函数作为程序开发人员的核心构造。开发人员可以以非常灵活的方式使用函数,例如将它们作为参数传递。利用Lambda表达式的这种能力,Java可以将函数用作方法的参数,或将代码用作数据。lambda表达式可以被认为是不与任何特定类关联的匿名方法。这些想法有一个非常丰富多彩和迷人的数学基础。函数式编程和Lambda表达式仍然是比较抽象和深奥的概念。对于开发者来说,主要关心的是如何解决实际生产中的任务,可能对跟踪最新的计算趋势不感兴趣。随着在Java中引入lambda表达式,开发人员需要了解这些新功能,至少要达到他们可以阅读其他开发人员编写的代码的程度。这些新特性也有实际好处——它们可以影响并发系统的设计以获得更好的性能。本文关注的是如何使用这些机制来编写简洁明了的代码。lambda表达式可用于生成简洁代码的原因有多种。局部变量的使用减少了,所以声明和赋值的代码也减少了。循环被方法调用所取代,将三行以上的代码减少为一行。原本位于嵌套循环和条件语句中的代码现在可以放在单个方法中。实现一个流畅的接口,允许以类似于Unix管道的方式将方法链接在一起。以函数式风格编写代码的最终效果超出了可读性。这样的代码避免了状态维护并且没有副作用。此代码还带来了更易于并行化、提高处理效率的额外好处。Lambda表达式与lambda表达式相关的语法比较简单明了,但它不同于Java以前版本的惯用语。Lambda表达式由参数列表、箭头和正文三部分组成。参数列表可能包含也可能不包含括号。此外,还增加了由双冒号组成的相关运算符,可以进一步减少一些特定的Lambda表达式所需的代码量。这也称为方法参考。线程创建在本例中,将创建并运行一个线程。lambda表达式出现在赋值运算符的右侧,指定一个空参数列表,以及在线程运行时写入标准输出的简单消息输出。Runnabler1=()->System.out.print("嗨!");r1.run()参数列表arrowbody()->System.out.print("Hi!");处理集合Lambda表达式的出现开发人员首先会注意到的地方之一与集合API相关。假设我们需要按长度对字符串列表进行排序。java.util.Listl;l=java.util.Arrays.asList(newString[]{"aaa","b","cccc","DD"});您可以创建一个Lambda表达式来实现此功能。java.util.Collections.sort(l,(s1,s2)->newInteger(s1.length()).compareTo(s2.length())该示例包含两个传递给Lambda表达式主体的参数,用于比较这两个参数的长度。参数列表箭头Body(s1,s2)->newInteger(s1.length()).compareTo(s2.length()));在不使用标准“for”或“while”循环的前提下,可以对列表中的每个元素进行操作。用于比较的语义也可以通过将Lambda表达式传递给集合的“forEach”方法来完成。这种情况下只传入一个参数,就不用括号了。forEach(e->System.out.println(e));ArgumentListArrowBodye->System.out.println(e)通过使用方法引用将包含类与静态方法分开,可以进一步减少代码量。每个元素按顺序传递给println方法。forEach(System.out::println)java.util.stream是Java8中新引入的一个包,它以函数式程序开发人员熟悉的语法处理集??合。包的内容在包摘要中解释如下:“为流元素的功能操作提供支持的类,例如集合的map-reduce转换。”下面的类图提供了包的概览,着重介绍了将在以下示例中使用的功能。包结构中列出了大量的Builder类。这些类,如流畅的接口,可以将方法链接到流水线操作集中。字符串解析和集合处理虽然简单,但在现实世界中有许多实际用例。在进行自然语言处理(NLP)时,您需要将句子拆分成单个单词。生物信息学将DNA和RNA表示为由C、G、A、T或U等字母组成的碱基。在每个问题域中,字符串对象被分解,然后根据其各个组成部分进行操作、过滤、计数和排序。因此,尽管示例中包含的用例非常简单,但这些概念适用于各种实际任务。下面的示例代码解析一个包含句子的String对象,并计算感兴趣的单词和字母的数量。包括空行在内,整个代码清单不超过70行。importjava.util.*;importstaticjava.util.Arrays.asList;importstaticjava.util.function.Function.identity;importstaticjava.util.stream.Collectors.*;publicclassMain{publicstaticvoidp(Strings){System.out.println(s.replaceAll([\\]\\[]",""));}privatestaticListuniq(Listletters){returnnewArrayList(newHashSet(letters));}privatestaticListsort(Listletters){returnletters.stream().sorted().collect(toList());}privatestaticMapuniqueCount(Listletters){returnletters.stream().collect(groupingBy(identity(),counting()));}privatestaticStringgetWordsLongerThan(intlength,Listwords){returnString.join("|",words.stream().filter(w->w.length()>length).collect(toList()));}privatestaticStringgetWordLengthsLongerThan(intlength,Listwords){returnString.join("|",words.stream().filter(w->w.length()>length).mapToInt(String::length).mapToObj(n->String.format("%"+n+"s",n)).collect(toList()));}publicstaticvoidmain(String[]args){Strings="Thequickbrownfoxjumpedoverthelazydog";Stringsentence=s.toLowerCase().replaceAll("[^a-z]","");Listwords=asList(sentence.split(""));Listletters=asList(sentence.split(""));p("句子:"+sentence);p("单词:"+words.size());p("字母:"+letters.size());p("\nLetters:"+letters);p("排序:"+sort(letters));p("Unique:"+uniq(letters));Mapm=uniqueCount(letters);p("\nCounts");p("字母");p(m.keySet().toString().replace(",",""));p(m.values().toString().replace(",",""));p("\nwords");p(getWordsLongerThan(3,words));p(getWordLengthsLongerThan(3,words));}}示例程序执行输出:Sentence:thequickbrownfoxjumpedoverthelazy狗词:9个字母:44个字母:t,h,e,,q,u,i,c,k,,b,r,o,w,n,,f,o,x,,j,u,m,p,e,d,,o,v,e,r,,t,h,e,,l,a,z,y,,d,o,g排序:,,,,,,,,a,b,c,d,d,e,e,e,e,f,g,h,h,i,j,k,l,m,n,o,o,o,o,p,q,r,r,t,t,u,u,v,w,x,y,zUnique:,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,t,u,v,w,x,y,zCountslettersabcdefghijklmnopqrtuvwxyz8111241121111114112221111字快|棕色|跳了|结束|懒惰5|5|6|适用于所有版本的Java,但某些方式可能不符合普遍接受的编码风格准则。想一想在早期版本的Java中如何实现相同的输出?首先,需要创建很多局部变量来临时存储数据或作为索引。其次,需要告诉Java如何通过许多条件语句和循环来处理数据。新的函数式编程风格更专注于需要什么数据,而不关心与之相关的临时变量、嵌套循环、索引管理或条件语句处理。在某些情况下,采用早期版本的标准Java语法来减少代码大小是以牺牲清晰度为代价的。例如,示例代码***行的标准import语句中的Java包,是指java.util下的所有类,而不是通过类名单独引用。对System.out.println的调用替换为对名为p的方法的调用,以便可以在每个方法调用中使用短名称(第9-11行)。这些更改是有争议的,因为它们可能违反了一些Java编码标准,但其他背景的开发人员查看代码可能没有任何问题。其他案例利用了自JDK8预览版以来才添加的功能。静态引用(第3-5行)可以减少内联需要引用的类的数量。正则表达式(第10、45行)可以以与函数式编程本身无关的方式有效地隐藏循环和条件语句。这些习语,尤其是正则表达式的使用,经常被质疑难以阅读和说明。如果使用得当,这些习语可以减少噪音,并可以限制开发人员需要阅读和解释的代码量。***,示例代码利用了JDK8中新的StreamingAPI。使用StreamingAPI中的多种方法(第17-40行)过滤、分组和处理列表。虽然它们与封闭类的关系在IDE中很清楚,但除非您已经熟悉API,否则这种关系并不那么明显。下表显示了示例代码中出现的每个方法调用的来源。方法完整方法名称引用stream()java.util.Collection.stream()sorted()java.util.stream.Stream.sorted()collect()java.util.stream.Stream.collect()toList()java.util.stream.Collectors.toList()groupingBy()java.util.stream.Collectors.groupingBy()identity()java.util.function.Function.identity()计数()java.util.stream.Collectors.counting()过滤器()java.util.stream.Stream.filter()mapToInt()java.util.stream.Stream.mapToInt()mapToObject()java.util.stream.Stream.mapToObject()uniq()(第13行)和sort()(第17行)方法体现了同名Unix实用程序的功能。结果被收集到一个列表中。UniqueCount()(第21行)类似于uniq-c,返回一个映射对象,其中每个键是一个字符,每个值是该字符出现次数的计数。两个“getWords”方法(第26和33行)用于过滤掉短于给定长度的单词。getWordLengthsLongerThan()方法调用一些额外的方法来格式化并将结果转换为不可修改的String对象。整个代码没有引入任何与lambda表达式相关的新概念。前面介绍的语法只适用于JavaStreamAPI的特定使用场景。总结用更少的代码来完成同样的任务的想法与爱因斯坦的哲学是一致的:“必须尽可能简洁,但绝不能简单地简化”。Lambda表达式和新的StreamAPI因其实现可扩展性的能力而受到很多关注。它们允许程序开发人员将代码适当地简化为最佳的表达形式。函数式编程惯用语被设计成简短的,仔细思考可以揭示许多可以使Java代码更紧凑的场景。新语法不熟悉但并不复杂。这些新特性清楚地表明Java已经远远超出了它作为一种语言的最初目标。它正在以开放的心态吸取其他编程语言的一些最好的特性,并将它们集成到Java中。