专栏名称: OSC开源社区
OSChina 开源中国 官方微信账号
目录
相关文章推荐
码农翻身  ·  漫画 | ... ·  昨天  
程序员的那些事  ·  趣图:看苹果手机发布会的感受 ·  3 天前  
OSC开源社区  ·  甲骨文正式发布VirtualBox ... ·  1 周前  
51好读  ›  专栏  ›  OSC开源社区

似乎我对 Java 存在误解 - Part 2

OSC开源社区  · 公众号  · 程序员  · 2017-01-23 08:32

正文


我和 Java 打交道并不多,所以我对于 Java 先入为主的观点是否正确,还有待考证。 上次,我主要探讨了 Java 给用户带来的影响,如速度和所占内存。 

探讨的结果是:不确定。

因为现在 Java 越来越多地应用在用户不可见的领域:如数据中心,所以对 Java 的评价主要来自开发人员。


Java 的不安全性


Java 的 infamy 安全问题可以追溯到很久之前,Java applet 问题比较常见,虽然 JVM 可以对它们进行沙盒处理,但偶尔也会出点岔子。

削减整个通用语言和其庞大的标准库来使它在 web 上安全运行的办法并不理想,现在 Java applet 的应用也比较少了,甚至没有 NPAPI 插件可以安装。Firefox 已经没有 Java applet 自动运行功能,并且在三月份完全放弃了对它支持;Chrome 也在去年减少了支持

之后 Java applet 遭受的攻击越来越多,甚至有不可用的风险;2014年,一份来自思科的 CISCO 报告突出显示,91% 的网络攻击目标就是 Java。在同一年,我的员工也被告知如果他们不是特别需要 Java,就在浏览器中手动将其关闭。显然,Java 没能给他们留下好印象。

以上是开发者的视角。就运行时本身而言,独立的 applet 又存在什么问题呢?“安全”很难量化,只能取近似值,以下是我今年寻找到的 CVEs 问题。

  • PHP: 107

  • Oracle Java: 37

  • Node: 9

  • CPython: 6

  • Perl: 5

  • Ruby: 1

数据显示 Java 的使用情况不太好,但其中原因尚不明确。


Java 过于企业化


所有人(包括我自己)都会对此颇有微词。它想在人们心中形成一个具体的印象,但定义确实非常模糊。对于它的意义,我有几点猜测。


Java 太过抽象


说到抽象性,该领域比较烂名的是 AbstractSingletonProxyFactoryBean

对于其抽象性的理解,可以用到 elasticsearch 下的 WhitespaceTokenizerFactory 类。它完整的源代码如下:

当你想要能够通过一些外部的状态来随意地创建出一个 tokenizer 时,你可能并不想让它对外部状态有所依赖。

Java 的代码很笨重,相同的词它重复了三遍;一个 38 行的文件实际只有两行有意义。就算是最糟糕的情况下,我想,用 Python 都比较好:

我运行了这段代码以看其实际效果。当然,这用 Java 也是可以实现的,但效果不会很好也不符合使用习惯。而 Python 代码自身就是基于 tokenizer 构建起来的。动态类型的一个好处就是代码可以使用一个类型,但不依赖这个类型。tokenizer 类可以同 IndexSettings 和 Environment 对象一起运行,而不必引入这些类型,甚至不必知道它们的存在。

为什么没在其它语言中看到同样的东西?

我花了一分钟在 GitHub 中多数标星的 Java 项目中随机点选,发现很多小型工厂类项目。这一点也不让我感到吃惊。但在其他显式的静态类型语言中,我不记得有发现类似的东西。C++ 中有小型工厂类项目吗?标星最多的 C++ 项目是 Electron,我搜索了 “factory” 只找到像这样的代码。 Objective-C 标星最多的项目是 AFNetworking,它只有一个 “factory”——在一个更新日志中。 Swift 标星最多的项目是 Alamofire,它压根没有包含 “factory”!

然后我了解到 C++ 风格类型系统中的间接层和小型类。我不明白为什么在 Java 和 C++ 中能看到这么多。

这是因为文化不同吗?C++ 开发者就这么喜欢错综复杂的相互依赖?这些 C++ 中的小型类,都存在于一个文件,难道是为了更容易让人忽视吗?

Java 看起来像是存在于抽象的封闭空间,但是我不明白为什么它与其它语言有这些差异。


Java 繁琐冗长


“企业” 让我想起繁琐的官僚作风,榨干了所有事物原本的快乐。

到处都是属性访问器

说到 Java 就让我想到它的访问器,跟官僚作风一样让人难受。

这些代码占用了大量的垂直空间却并没有发挥作用。我完全可以用 public int foo 来代替这些代码。

世界上有三种程序员,他们由对上面这段话的不同反应区分开来。点头说不错的可能是 Python 程序员。当然也有人对此表示反对,认为这违反了封闭原则,而且如果我说我不关心封装,他们会再一次反对。最后一类人会转溜眼睛略加思考,然后指出 public 属性会固化在 API 中,以后如果不打破现有的代码就无法改变它。

而正是最后那批人说到了点上。因为问题在于 Java 不支持属性。“属性” 在语言特性中是个不被看好的通用名称,最近才开始变得流行起来。如果你不熟悉这种在 Python 中存在的神奇的东西,你或许会认为它还不错。如果你有一个属性叫 foo,外部代码可以随意对它进行修改,稍后当你决定让它只能被设置为奇数时,你可以这样做一些处理,并且不会破坏 API 的约定:

@propertyis 是一个强大的神器,它能轻易拦截对属性的读或写操作。其它使用 obj.foo 的代码会毫无察觉的继续工作。甚至 @property 本身也能通过基本的 Python 代码来表达,另外,它还包含一些有趣的变体:懒加载属性、作为清晰操作弱引用的属性等。

我知道 Python、Swift 和一些 .NET 语言(C#、F#、VB、Boo...)支持属性(property)。 到目前为止,虽然我不知道有多少代码原生地依赖于属性,但 JavaScript 被认为是支持属性的。 Ruby 中有属性的概念,只是使用了稍有不同的语义。Lua 和 PHP 中可以伪造属性。Perl 也有属性概念,但你可能不会用它。因为 Jython 和 JRuby 的存在,JVM 本身必须能够支持属性。那 Java 语言为什么不支持(属性)呢?
我感到奇怪,为什么 Java 没有选择实现属性的功能,这明明可以删减很多重复。这显然是为 Java 7 提出的,但是我找不到为什么它不做这些删减的理由,虽然
这也不是很重要

别着急,还有更多

在这里我显示了 Python 的颜色,但不止这些:另一个窍门是这个类在模块加载时很容易操纵。一个类只是在执行创建类代码的时候定义。所以 Python 还有一些有趣的小把戏,如 attrs 模块, 它允许:

用这样的方式来声明属性,你可以轻易获得:一个按顺序或按名称接受参数的构造函数;一个合适的 repr(像 toString,但明确地仅用于调试); hash 算法; 比较运算符; 和双向确认的不变性。但是这并不会有代码生成,因为它们只是一些快速操纵的类,只在运行时才定义。
显然,精确的方法不能在 Java 中自由运用,不过它可以被模拟。我知道 Java IDE 由于它们产生的代码生成量而臭名昭著。所以我有点惊讶 Java 自身没有采用一种方法在编译时来生成或重写代码。

适当地重用

同样地,以下类型定义似乎是常见的恼心事:

在这里小的类型定义会更加适用。Java 近期确实增加了一些类型接口,但是仅仅适用于泛型:


这的确是一种改善,但我很困惑为何此特性后续没有继续完善了。事实上,这一改善尚未达到我的期望,因为第一步通常是依据表达式的类型推断变量的类型,而不是其他方式。我之前看过一些 Java 10 中关于更传统的类型接口的推测,如果这些在未来能实现的话,将会是非常不错的改善。

ComicallyLongStrawmanTypeName

ComicallyLongStrawmanTypeName 也值得一提。Java 因其类型名称超长而不被待见。我过去从未想过这种情况的原因,但现在几乎可以肯定,是由软件包系统的设计造成的。

包名往往至少含有域名和项目名两部分,例如 org.mozilla.rhino,它已经有点绕嘴了。更麻烦的是,包名和类名还不能起别名。所有的包也没有层级从属关系,所以你无法导入一个包的完整“分支”。如果你想使用一个包中的一个类,你有两种方式:直接使用 org.mozilla.rhino.ClassName,或者将包导入后使用 ClassName。

但结果就导致类名必须加以限制以避免命名冲突!如果你把一个类命名为 List,就会给其他想要使用同一个文件中的标准库列表的类的人造成困扰。所以最终你把类命名成 FooBarList 并置于包 com.bar.foo 下。

这似乎有点违背分包的目的。在 Python 里,我可以给包起别名,可以导入父级包,还可以给类起别名。我可以给一个文件里的类都起成很通用的名字,然后通过短的别名将该文件导入, 再以 pkg.List 的方式使用。这一点很不错,显著地减少了重复的信息。但是在 Java 里,你为类命名时必须将其当作一个全局命名空间的一部分,因为这些类可以同其他任何类一起导入。

(顺便说一句,这种微妙的全局命名空间也让我对 Java 风格的接口产生了怀疑。一个接口里的方法名同其他所有 Java 代码里的方法名共享一个全局命名空间——因为接口的目的就在于,一个类可以实现任意组合的接口功能。所以你也无法在这使用优雅的短名称,否则就会有命名冲突的风险。但这并不是批评 Java——因为很多语言也存在同样的问题,包括 Python,尽管 Python 缺少明确的接口,但问题不大。Rust 则采用不同的接口实现方式,而不把接口当作类体的一部分,从而在很大程度上避免了这一问题。我相信这个想法来自于 ML 家族。)

让我印象很深的一点就是,Java 有个地方确实有点简练:省略 this。但我不是很喜欢省略 this。这会在浏览文件时造成麻烦,比如,当我看到这个时:

在不查看这个方法其余部分的情况下,我没办法只看一眼就判别这是一个局部变量还是一个类属性。一定会有很多人不同意我的看法,因为省略 this 真的没节省多少空间,也没让你少打几个字,所以我很奇怪,就在其他语言特性负责变得更加冗长时,Java 做了一个这么奇怪的“优化”。

我有点惊讶,我原以为 Java 繁琐冗长应该是种文化现象而不是一种语言属性, 但似乎 Java 本身却无意地促进了冗长代码的发展。最近 Java 语言的一些变化确实鼓舞人心,我也希望今后能看到更多的改善。


一个简单的目标


此刻, 我开始意识到,保持 API 的稳定性是造成 Java 语言大部分冗长的真正原因:

  • public 和 private 作为稳定接口的一部分,声明了你打算支持的内容。

  • 存取方法使得接口永不过时,而且允许在后面向接口增加新的逻辑。

  • 静态类型使接口尽可能保守。

  • 接口和其他形式的间接引用尽可能地减少代码间的依赖。

  • 继承也是每个类 API 的一部分,它通常是受保护类和不可变类。

  • 针对以后可能改变返回值类型的情况,工厂模式保证了扩展性,而利用构造器做不到这一点。

  • 导入确实比较死板

但是,认识到稳定性之后, 我对 Java 有了不一样的印象。

如果人人都想要保证的是一个稳定的 API,为何核心语言和工具没法将其实现呢?相反,我们有一大堆间接的方法,如公有和私有,我们必须有意识的记住不破坏 API 兼容性的允许改变的列表。如果我们偶然有了破坏性的改变,除非有一个依赖于 API 的测试,不然我们无法察觉到。为什么电脑不能自动检测该保证?为什么在这些基本要素显然是很有必要的情况下,没有任何一个主流语言将其集成了呢?

同样,Java 无法表明哪部分 API 属公共使用。方法可以是私有的或共有的;类也可以是私有的或公有的;但我不知道该用何种方式来说明整个文件/目录是公共 API 还是仅仅是内部工具包。“包私有”的可见性似乎用得上,但因为包不是分层的,除非你将整个项目封装到一个包,不然也派不上用场了。(我唯一熟悉的能很好处理该问题的语言是 Rust。)解决办法或许是保持一组独立的公共包装类,但如果公共接口完全分离,内部代码中遍布的稳定性关键词又意义何在呢?也许 Java 9 的模块系统能改善这一点。

想起来了,我想知道:是否多数 Java 开发人员都会为了内部代码而放弃稳定性?我想,在你自己的代码库中使用 private 毫不费力,但使用 public 也是如此。多次听人提起 Java 的 IDE 具有近乎神奇的重构能力,所以我可以确定,在必要时一个属性可以被自动更改为使用访问器进行访问。那么,有人利用这项功能来避免使用不会过时的模板么?

我猜答案是“没有”,因为访问器被视为最佳实践 —— 或者说由于其固有的优良特性,使其成为面向对象设计的结构中不可或缺的一部分。如果是这样,那就同 Python 形成了有趣而鲜明的对比,Python 里的访问器通常被视为多余的——因为该语言提供了一个可以改变普通属性而不会破坏 API 的方法。

与之类似,Python 也根本没有“私有”的概念。按照惯例,以单个下划线开始的方法名或属性名代表“私有”,但这只意味着“如果这个以后变了可不要怪我”,并且对核心语言而言几乎没有区别。当一个第三方的库几乎满足我的要求,但在我需要的地方又没有一个钩子时,这一点就非常有用。如果不介意代码变得有点脆弱,那我可以只定义一个子类,再封装一个“私有”的方法。按照这种方式我可能会不小心破坏某个对象的稳定性,但我知道这只是由于我自己的愚蠢错误造成的。这有助于保证初始的源代码仍然可用。

但我已经看到一部分人坚持认为 Python 没有“真正的”或“全面的”OO 支持,仅仅是因为它缺乏这些 Java 功能。似乎“好的 OO 设计/支持”就等价于“Java 支持的所有功能”。这算是一个强大的品牌力量。
这是违反直觉的,设计准则在两种语言之间如此彻底地悖反。封装被认为是好的,因为它隐藏了对象的实现,但有时对象本身是它的表示,而 Java 中并没有合适的表达方式。主机名不是 URL 功能的一部分,它是 URL 实现的一部分。我认为以问题或命令(即方法调用)的形式表达“此 URL 的主机名”似乎不太合适。但在 Java 中,你没有选择。我也遇到过认为 Java 封装是完美的开发人员,他的观点就和 OO 原则是根据 Java 自身的限制来定义的一样。
我不打算指责 Java 或 Java 开发人员在这方面的问题。我相信在 Python 代码中能学到更加奇异的原则。但是我对我们自己的文化背景以及它告诉我们的感知世界的方式异常着迷。
现在,我已经走到哪一步了呢?


Java 太保守了


啊,现在我们可以进行下一步讨论了。

总之,在过去这些年里,这门语言相比较而言变化很少。在 2004 年,它增加了泛型和注解以及其他一些细节,在 2014 年还增加了 lambda 表达式,但是没有太根本的改变。相反,甚至 C++ 在这些年里已经发生了一些重大的变化。

我喜欢编程语言有新意或者冷人振奋的发展,但是我也理解这需要以一种缓慢而平稳的方式进行。C 语言一生中只有三个半的版本: C89,C99,C11,加上可能的C95。这一语言小但是足够稳定,从20世纪90年代开始,前沿的编译器仍然被期望用来处理代码。接口非常简单,它在正确的时机下链接新的库来编译代码 my age。这是一个吸引人的技术。

代价是 C 不会为你带来效率上的重大提升,所以不得不重复造轮子。同时,C很少移除甚至让某些特性过期;众所周知 C 语言的雷区被标记在一系列特定的编译器警告中。

另一方面,Python 在过去五年中一共有四个发布版本。过时的功能或库偶尔会被弃用,并在后续的几个版本之后删除,因此,如果 Python 代码希望在后续的 Python 版本中工作,可能需要一些轻量级的维护。Python 也没有稳定的编译格式来隔离已部署的库受到语言变化的影响。虽然语言本身被指定得足够好,并且存在若干替代的实现,但是它们往往落后几个发布版本。
Java 似乎是定位于有趣的妥协。核心语言增长相对缓慢,这避免了日后消除错误的需要。标准库是相当广泛的,但如果这个 
deprecations 列表表明了什么迹象的话,那就是旧的 API 很少被彻底删除——我看到了 Java 1.3 上的功能,这在十六年前已被弃用了。
这表明了对稳定性和兼容性的强烈关注。新功能非常小心地添加,特别是与核心语言相关的。作为补充,Java 生态系统已经在其之上构建了大量的工具;第三方代码可以更自由地进行验证,保守的开发人员可以自由地选择不使用这些工具。我注意到,Java 注释最初是一个 javadoc hack,所以看起来是核心语言采纳了来自生态系统的流行观点,这是很好的。

不利的一面是,Java 已经有了一些有名的代码生成器,由核心语言来做层的配置/反射是困难的,很多 XML 都有它存在的理由。

另一方面,保持语言的稳定增长让 JVM 更容易实验,这导致相继打开了几门新的语言。Clojure, Scala, Groovy 可能不存在一个坚实的 VM 和一个巨大的生态系统及可用库用来处理。

用 “保守的” 来形容 Java 还是太表面了。更准确地说,Java 限制了一些东西,它的慷慨之处很微妙。对于企业来说,保守主义还是存在很大的风险,毕竟,这绝对是一个特质相关联的世界。


启示


沿着这条弯曲的道路,我来畅想下别的东西。我对核心语言的诸多厌恶实际上是 C++ 带来的。
Getters,setters,再也没有属性了,就像 C++ 那样。
访问控制与 C++ 类似,即访问对象的内部可能会导致内存崩溃。
每个文件仅包含一个类,与 C++ 类似。
大括号,分号和驼峰命名规则与 C++ 一样。这个问题很棘手,我认为大括号是干扰项。
类名重复的构造函数名和 C++ 一样。使用 public new() 或其他规则可能更好。
能够先使用类型,而不需要显式导入,这一点和 C++ 一样。虽然 Java 能更好地处理这一点,但遍历导入项,仍然不足以找出一个文件所有的依赖项。

空值也和 C++ 一样,它使整个类型系统毫无意义。类型为 T 的变量可能包含 T 的值,或者它也可能包含 null,而这不属于类型 T 的值。没一个类型注解是可靠的,甚至连 Option 也可能是 null!


最后


Java 有几个缺点。语言设计本身带来了一些繁琐的开发耗费;平台本身的安全问题非常常见;而且变化缓慢,不论好坏。我仍然不知道为什么前文中的两个程序消耗这么多内存。
是我对 Java 存在误解吗?有一点。Java不像我想象的那么糟糕,但它也不是特别好。Java 的原始设计理念可能是“没有那么多毛病的 C++”,按照这个标准,Java 绝对是成功的。我知道 Java 不是为我设计的,但 Java 的存在是作为某些也不是为我设计的编程语言的替代品。嘿,我发现更多的事情都和 C++ 有关,这总是让我开心。
自 Java 首次发布以来,我们的世界已经发生了很大变化。现在我们拥有了:一个完整的 .NET 竞争生态系统、JIT 如 LuaJIT 和 PyPy 弹出,甚至是用 JavaScript 编写的桌面软件,并可部署自己的小型网络浏览器。Java 或许只能负责被广泛接收的 VM 和 JIT,我为此感到很欣慰。(好吧...有待印证。)

如果你发现任何有趣并且之前没有做过的事,你或许可以用 Python 或 Ruby 试一下,我最初就是用这两项语言做开发的。它们都有繁荣的社区,里面有非常聪明的人,并对于面向对象的具体含义都持有不同的观点。Python 在表面上看起来更像 Java 一点,虽然下面隐藏了一个非常灵活的原型对象模型; Ruby 使用 Smalltalk 的消息传递方法,并添加了一个来自 Perl 的轻量级 dash。你甚至可以同时尝试这两个;最糟糕的事情也就是你转而更喜欢 Java 吧。




推荐阅读

程序员排行榜:测测你的码力值,2016年击败了全国多少工程师?

时隔半年,Docker 发布重大版本 1.13.0;Debian GNU/Linux 8.7 稳定版发布 | 软件周刊

羽毛也疯狂,盘点 Apache 最新毕业的11个顶级项目

2016 年度开源中国新增开源软件排行榜 TOP 100

2016 年度最受欢迎中国开源软件 TOP 20

点击“阅读原文”查看更多精彩内容