笔者近期花了点时间来 “拓宽了自己的语言池”。有些是出于功利角度 ( 如 Java 的 Web 开发,Scala 的大数据框架 ),有些则完全出于兴趣 ( 如 Groovy,Go ),在学习不同语言的过程中多少了解到了不同的编程思路和风格。不过最近同时学习的东西难免太多了,导致笔者在不断切换语言的过程中思路发生了一点混乱。这里通过一些小短文总结笔者所学习的 Java,Scala,Groovy,Go 在语言特性的一些异同,以便于日后能在需要的场合快速回忆起如何操纵它们。
如果要快速上手一门新语言,笔者率先会以下角度入手:数据类型,函数定义,依赖导入。数据结构永远是通用的:比如数组,列表,映射,还有一些日常开发中的常用操作,HTTP,数据库连接,Socket, IO 流。笔者认为,了解完这些,基本能够去逐步去上手一些开发任务了 ( 深入挖掘语言特性的除外 ),剩下的要做的就是通过合适的开发框架来解决需求。
1. 梗概
Java:语法比较固定,开发时无需纠结语法,团队开发时沟通成本和开发成本都很低。受众最多,有任何问题都能在网上查到。
Scala:大量隐式转换,原生支持函数柯里化,高阶类型,类型型变,有非常灵活的操作符重载机制,以及随处可见的 _
符号 ( 这个符号让 Scala 程序员升华到了 “意识流编程” 的境界 )。无拘无束的语法导致不同开发者的 Scala 代码风格迥异,不太适合在大型团队协作时使用。此外,IDE 对语言本身的支持性一般 ( 靠安装插件凑合过日子 ),囿于复杂的编译机制,有些错误提示晦涩难懂。
Groovy:动态类型判断,元编程,方法运行时注入,为 JDK 包了不少 “糖衣”,适合去做一些基于 JVM 的自动化维护。然而,高甜度和高动态性的代价是牺牲运行性能,和 Scala 一样没有 “趁手” 的 IDE ( 也是靠安装插件 ),偶尔会有一些来自 IDE 的 Bug。
Go:提供基于 CSP 理论的通信编程 goroutine
& chan
,天生具备异步编程的能力。支持短变量命名,不允许出现未使用的库引用或者变量;有指针的概念,但是也具备 GC 机制;跨平台肯定不如 JVM 语言,至少需要把源代码拿到其它平台重新 build go
一下。有专门的 IDE GoLand,代码写起来很舒服。
2. 编程范式
Java:仍旧以 OOP 为主,FP 体验一般 ( 甚至很差 ),因为 Java 会强迫我去记忆各种函数式接口的名字。
Scala,Groovy:支持 FP 和 OOP 两种编程风格,Scala 对函数类型有非常直观的符号表达,Groovy 则将一切闭包都视作 Closure<ReturnType>
。
Go:函数即一切。Go 抛弃了固有的 OOP 模式,因此也不存在继承,多态 的概念 ( 但不意味着 Go 就不 OOP 了 )。Go 更鼓励以组合的形式实现代码复用。
3. 关于静态
这四个语言中,只有 Java,Groovy 有 “静态” static
的设定。
Scala 认为这违背了 OOP,因此 “静态” 的概念被伴生对象 object
替换掉了;Go 则认为 “静态” 的内容完全可以使用纯函数或者是包变量代替,所以它也没有 static
的概念。
4. 访问权限控制
Java:public
,protected
( 包和子类可见 ),缺省 ( 仅本包可见 ),private
。
Scala:缺省即表示 public
,其次是 protected
( 仅对子类可见 ),private
。额外提供包变量使包内成员共享数据。
Groovy:缺省即表示 public
,其次是 protected
( 包和子类可见 ) 和 private
。实际上,编译器对跨权限访问无能为力 ( 仅仅给出警告 ),因为 Groovy 有的是办法 “合法地” 访问它。
Go:不需要任何关键字,仅通过变量 / 函数首字母的大小写来决定它们能否导出。
5. 数据结构的划分
Groovy 基本遵循 Java 传统的 8 个基本数据类型:byte
,short
,int
,long
,float
,double
,char
,boolean
。
Scala 在 Java “老八珍” 的基础之上做了封装,每种类型都有 toXXX
方法供强制转换;具备顶类型 Any
以及底类型 Nothing
;Scala 非常在意一个变量应当是不可变的 val
还是可变的 var
。
Go 语言则主要是根据长度对数据进行划分:int8
~ int64
( 包括无符号的 uint8
~ uint64
),float32
,float64
。有一种特殊的类型:无符号类型,用于维持常量的高精度。
6. 对字符,字符串的处理
Java 的 char
表示一个 “字符”,在内部对 Unicode 字符集进行 utf-16
编码,固定为 2 个 byte
( 极少部分字符可能需要两个 char 存储 )。当数据被序列化到外部时,采用 utf-8
编码方式,此时一个字符长度从 1 ~ 3 byte
不等。JVM 的字符串由 java.lang.String
类实现,内置了各种字符串操作方法。
在此基础上, Groovy 将所有的字符,字符串全部包装成了 String 或者 GString。
Go 语言有原生的 string
类型,编码方式统一采用 utf-8
,而截取 string
的单个元素却只能得到 byte
。Go 语言的单个字符使用 rune
来表示,一些涉及字符的操作要首先将 string
强制转换到 []rune
( 这样来看,Go 的 []rune
更像是 Java 中的 String
) ,比如 “查字数” 需要调用 utf8.RuneCount()
函数。字符串处理一般要依赖 strings
,strconv
,unicode
包。
7. 类型与类型推导
Java:JDK 11 开始可以使用 var
令编译器进行局部变量的类型推导。支持泛型,支持泛型上下界,但不支持泛型型变 ( 数组支持协变 )。不支持高阶类型。
Groovy:与其说是类型推导,不如说将类型验证延迟到了运行时。
Scala:原生支持类型推导,支持泛型,支持型变的概念,除此之外还有上下文界定和视图界定。支持高阶类型,可通过反射时获取。
Go:原生支持类型推导,目前不支持泛型 ( 以后应该也没有 )。
8. 对接口的定义
Java:在 JDK 8 之后,支持设置默认方法,静态方法,接口常量;在 JDK 9 之后,支持设置私有方法。
Groovy:允许使用动态地将多个闭包组合成一个 Map,并将其声明为一个接口的实现。
Scala:允许在创建对象时动态混入并叠加多个特质,以及专门服务于某个类的自身特质。
Go:无需主动声明实现了接口,只要一个类型 struct
实现了接口的所有方法,那它就可以被当作这个接口去使用 ( 笔者最喜欢的一种方式 )。
9. 关于函数定义
Java:平平无奇。
Scala:原生支持柯里化,分为传名调用和传值调用。
Groovy:函数被称作闭包,其它 FP 应有的特性都具备。
Go:支持返回多个值,支持 defer
调用,可以很方便地通过 Execute Around Method ( 类似于 AOP ) 模式关闭资源。
10. 循环结构
Groovy:Java 的语法在 Groovy 同样适用,额外支持 for(... in ..) {}
实现遍历 ( 像 Python 那样 )。
Scala:for
表达式的信息熵非常大,能够表达 map
,flatMap
,foreach
,withFilter
等语义,反过来说,这些语义的组合可以使用 for
表达式简化。推崇递归解决问题,编译器可将尾递归函数转换为等效的迭代代码。
Go:只使用 for
表达式,可以配合 range
关键字实现对序列,映射的遍历。
11. 异常处理
Java:受检异常必须 try-catch
,或者 throws
抛给上级。
Scala,Groovy:不要求主动处理 Java 中的受检异常 ( 大概都觉得 Java 的这个东西太毒瘤了 )。
Go:除了严重的问题使用 panic
宕机之外,一般异常 ( error
接口 ) 都以返回值的形式传递给外部。
12. 有关于数组
在 Go 语言中,数组的长度属于数组类型的一部分,拥有独特的切片类型。其它三种大同小异。
13. 有关于常用集合
Java:常用的是 List
,Map
两种接口 ( Set
表示数学意义上的集 )。每个接口下有多种实现,可以根据实际情况 ( 随机访问多?还是插入操作多?插入键值对时有必要记录插入次序吗?) 来选择合适的类型。对它们进行 map
,filter
等变换操作时需要切换到 Stream。
Groovy:沿用了 Java 的 ArrayList
和 LinkedHashMap
,支持直接对集合做变换操作。
Scala:主要是强调可变和不可变两种类型,支持直接对集合做 ( 很多复杂的 ) 变换操作。
Go:通过 make(map[key]value)
创建映射,通过 list.new()
创建列表,没有直接地提供集合变换操作。