转载

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

我和 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 类。它完整的源代码如下:

public class WhitespaceTokenizerFactory extends AbstractTokenizerFactory {

    public WhitespaceTokenizerFactory(
            IndexSettings indexSettings,
            Environment environment,
            String name,
            Settings settings) {
        super(indexSettings, name, settings);
    }

    @Override
    public Tokenizer create() {
        return new WhitespaceTokenizer();
    }
}

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

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

@builder_for(WhitespaceTokenizer)
def build(cls, index_settings, env, name, settings):
    return cls()

@builder_for(SomeOtherTokenizer)
def build(cls, index_settings, env, name, settings):
    return cls(index_settings.very_important_setting)

# etc.

我运行了这段代码以看其实际效果。当然,这用 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 就让我想到它的访问器,跟官僚作风一样让人难受。

private int foo;

public int getFoo() {
    return this.foo;
}

public setFoo(int foo) {
    this.foo = foo;
}

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

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

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

class Bar:
    def __init__(self):
        # Leading underscore is convention for "you break it, you bought it"
        self._foo = 3

    @property
    def foo(self):
        return self._foo

    @foo.setter
    def foo(self, foo):
        if foo % 2 == 0:
            raise ValueError("foo must be odd")
        self._foo = foo

bar = Bar()
bar.foo = 8  # ValueError: foo must be odd

@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 模块 , 它允许:

import attr

@attr.sclass Point:
    x = attr.ib(default=0)
    y = attr.ib(default=0)

用这样的方式来声明属性,你可以轻易获得:一个按顺序或按名称接受参数的构造函数;一个合适的 repr(像 toString,但明确地仅用于调试); hash 算法; 比较运算符; 和双向确认的不变性。但是这并不会有代码生成,因为它们只是一些快速操纵的类,只在运行时才定义。

显然,精确的方法不能在 Java 中自由运用,不过它可以被模拟。我知道 Java IDE 由于它们产生的代码生成量而臭名昭著。所以我有点惊讶 Java 自身没有采用一种方法在编译时来生成或重写代码。

适当地重用

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

ComicallyLongStrawmanTypeName value = new ComicallyLongStrawmanTypeName();

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

List<ComicallyLongStrawmanTypeName> value = new ArrayList<>();

这的确是一种改善,但我很困惑为何此特性后续没有继续完善了。事实上,这一改善尚未达到我的期望,因为第一步通常是依据表达式的类型推断变量的类型,而不是其他方式。我之前看过一些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。这会在浏览文件时造成麻烦,比如,当我看到这个时:

foo = 3;

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

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

原文  https://www.oschina.net/translate/maybe-i-was-wrong-about-java-part-ii
正文到此结束
Loading...