编码Java时的10个微妙的最佳实践

这是10个最佳实践的列表,这些最佳实践比您的平均Josh Bloch有效Java规则要微妙得多。 尽管Josh Bloch的列表很容易学习,并且涉及日常情况,但此处的列表包含了涉及API / SPI设计的较不常见的情况,但可能会产生很大的影响。

我在编写和维护jOOQ时遇到了这些问题, jOOQ是Java中的内部DSL建模SQL。 作为内部DSL,jOOQ最大限度地挑战了Java编译器和泛型, 将泛型,可变参数和重载组合在一起,这是Josh Bloch可能不推荐使用的“平均API”。

让我与您分享编码Java时的10个微妙的最佳实践:

1.记住C ++析构函数

还记得C ++析构函数吗? 没有? 然后,您可能会很幸运,因为您无需再调试任何代码,因为删除对象后没有释放分配的内存,因此不会留下内存泄漏。 感谢Sun / Oracle实现垃圾回收!

但是,尽管如此,破坏者还是有一个有趣的特征。 通常以相反的顺序释放内存是有意义的。 在使用类似析构函数的语义进行操作时,也要在Java中记住这一点:

  • 当使用@Before和@After JUnit批注时
  • 分配时,释放JDBC资源
  • 调用超级方法时

还有其他各种用例。 这是一个具体示例,显示了如何实现某些事件侦听器SPI:

@Override
public void beforeEvent(EventContext e) {
    super.beforeEvent(e);
    // Super code before my code
}

@Override
public void afterEvent(EventContext e) {
    // Super code after my code
    super.afterEvent(e);
}

另一个臭名昭著的餐饮哲学家问题就是一个很好的例子,说明了为什么这很重要。

at_the_table

餐饮哲学家。 在这里看到: http : //adit.io/posts/2013-05-11-The-Dining-Philosophers-Problem-With-Ron-Swanson.html

规则 :无论何时使用before / after,allocate / free,take / return语义实现逻辑,请考虑after / free / return操作是否应按相反的顺序执行操作。

2.不要相信您早期的SPI发展判断

向消费者提供SPI是允许他们将自定义行为注入您的库/代码中的简便方法。 不过请注意,您的SPI演变判断可能会欺骗您,使您认为(不需要)该附加参数 。 确实, 不应及早添加任何功能。 但是一旦发布了SPI,并决定遵循语义版本控制 ,当您意识到在某些情况下可能还需要另一个参数时,您会后悔自己在SPI中添加了一个愚蠢的单参数方法:

interface EventListener {
    // Bad
    void message(String message);
}

如果还需要消息ID和消息源怎么办? API的发展将阻止您轻松地将该参数添加到上述类型。 使用Java 8,您可以添加防御者方法来“捍卫”您糟糕的早期设计决策:

interface EventListener {
    // Bad
    default void message(String message) {
        message(message, null, null);
    }
    // Better?
    void message(
        String message,
        Integer id,
        MessageSource source
    );
}

请注意,不幸的是,防御者方法不能设为final

但是,比使用数十种方法污染SPI更好的方法是,仅为此目的使用上下文对象(或参数对象)

interface MessageContext {
    String message();
    Integer id();
    MessageSource source();
}

interface EventListener {
    // Awesome!
    void message(MessageContext context);
}

与EventListener SPI相比,您可以更轻松地开发MessageContext API,因为实施该应用程序的用户将更少。

规则 :无论何时指定SPI,都应考虑使用上下文/参数对象,而不要编写带有固定数量参数的方法。

备注 :通常也可以通过专用的MessageResult类型(可以通过构建器API构造)来传递结果,这是一个好主意。 这将为您的SPI增加更多的SPI演进灵活性。

3.避免返回匿名,本地或内部类

Swing程序员可能有几个键盘快捷键可以为其数百个匿名类生成代码。 在许多情况下,创建它们很不错,因为您可以本地遵守接口,而无需经历思考完整SPI子类型生命周期的“麻烦”。

但是,您不应该过于频繁地使用匿名类,局部类或内部类,原因很简单:它们保留对外部实例的引用。 并且,如果您不小心,它们会将外部实例拖到任何地方,例如,拖到本地类之外的某个范围。 这可能是内存泄漏的主要来源,因为整个对象图会突然以微妙的方式纠缠在一起。

规则 :每当编写匿名,本地或内部类时,请检查是否可以使其成为静态类,甚至是常规顶级类。 避免将匿名,本地或内部类实例从方法返回到外部作用域。

备注 :对于简单对象实例化,围绕双花括号有一些聪明的做法:

new HashMap<String, String>() {{
    put("1", "a");
    put("2", "b");
}}

这利用了JLS§8.6中指定的 Java实例初始化程序 。 看起来不错(也许有点奇怪),但确实是个坏主意。 原来是完全独立的HashMap实例现在将保留对外部实例的引用,无论发生什么情况。 此外,您将创建一个其他类供类加载器管理。

4.立即开始编写SAM!

Java 8正在敲门。 随Java 8一起提供lambda ,无论您是否喜欢。 不过,您的API使用者可能会喜欢它们,因此您最好确保他们可以尽可能多地使用它们。 因此,除非您的API接受简单的“标量”类型(例如intlongStringDate ,否则您的API应尽可能多地接受SAM。

什么是SAM? SAM是单一抽象方法[Type]。 也称为功能接口 ,很快将使用@FunctionalInterface注释进行注释 。 这与规则2配合得很好,其中EventListener实际上是SAM。 最好的SAM是具有单个参数的SAM,因为它们将进一步简化lambda的编写。 想象写作

listeners.add(c -> System.out.println(c.message()));

代替

listeners.add(new EventListener() {
    @Override
    public void message(MessageContext c) {
        System.out.println(c.message()));
    }
});

想象一下通过jOOX进行的 XML处理,它具有几个SAM:

$(document)
    // Find elements with an ID
    .find(c -> $(c).id() != null)
    // Find their  child elements
    .children(c -> $(c).tag().equals("order"))
    // Print all matches
    .each(c -> System.out.println($(c)))

规则 :与您的API使用者友好, 现在已经编写SAM /功能接口。

备注 :有关Java 8 Lambda和改进的Collections API的一些有趣的博客文章可以在这里找到:

5.避免从API方法返回null

我曾经写过一两次关于Java的NULL的博客。 我也写了关于Java 8对Optional的介绍的博客。 从学术和实践的角度来看,这些都是有趣的话题。

尽管NULL和NullPointerExceptions在Java中可能会持续一段时间,但是您仍然可以通过设计API来避免用户遇到任何问题。 尽可能避免从API方法返回null。 您的API使用者应能够在适用的情况下链接方法:

initialise(someArgument).calculate(data).dispatch();

在上面的代码段中,所有方法都不应该返回null。 实际上,通常使用null的语义(缺少值)应该是非常例外的。 在诸如jQuery (或jOOX ,其Java端口)之类的库中,由于始终对可迭代对象进行操作 ,因此完全避免了null。 是否匹配某项与下一个方法调用无关。

由于延迟初始化,通常还会出现空值。 在许多情况下,也可以避免延迟初始化,而不会对性能产生重大影响。 实际上,仅应谨慎使用惰性初始化。 如果涉及大型数据结构。

规则 :尽可能避免从方法返回null。 仅将空值用于“未初始化”或“不存在”的语义。

6.切勿从API方法返回空数组或列表

虽然在某些情况下从方法返回null可以,但是绝对没有用过返回null数组或null集合的用例! 让我们考虑一下丑陋的java.io.File.list()方法。 它返回:

在此抽象路径名表示的目录中命名文件和目录的字符串数组。 如果目录为空,则数组为空。 如果此抽象路径名不表示目录,或者发生I / O错误,则返回null。

因此,处理此方法的正确方法是

File directory = // ...

if (directory.isDirectory()) {
    String[] list = directory.list();

    if (list != null) {
        for (String file : list) {
            // ...
        }
    }
}

空检查真的必要吗? 大多数I / O操作都会产生IOException,但是此操作返回null。 Null无法保存任何指示为什么发生I / O错误的错误消息。 因此,这在三种方式上是错误的:

  • 空无助于发现错误
  • Null不允许将I / O错误与不是目录的File实例区分开
  • 每个人都会忘记空值

在集合上下文中,“空缺”的概念最好通过空数组或集合来实现。 除了再一次进行延迟初始化外,几乎没有有用的数组或集合。

规则 :数组或集合绝不能为空。

7.避免状态,发挥作用

HTTP的优点在于它是无状态的。 所有相关状态都在每个请求和每个响应中传递。 这对于REST的命名至关重要: 代表性状态转移 。 当用Java完成时,这也很棒。 当方法接收有状态参数对象时,可以根据规则2来考虑它。 如果状态在此类对象中传递,而不是从外部进行操纵,则事情会变得更加简单。 以JDBC为例。 以下示例从存储过程中获取游标:

CallableStatement s =
  connection.prepareCall("{ ? = ... }");

// Verbose manipulation of statement state:
s.registerOutParameter(1, cursor);
s.setString(2, "abc");
s.execute();
ResultSet rs = s.getObject(1);

// Verbose manipulation of result set state:
rs.next();
rs.next();

这些使JDBC成为难以处理的API。 每个对象都是难以置信的有状态且难以操纵。 具体来说,有两个主要问题:

  • 在多线程环境中正确处理有状态的API非常困难
  • 很难使有状态资源在全球范围内可用,因为没有记录状态

http://en.wikipedia.org/wiki/File:Forrest_Gump_poster.jpg

阿甘正传的戏剧海报, 派拉蒙影业Paramount Pictures)版权所有©1994。 版权所有。 可以相信上述用法满足了所谓的合理使用

规则 :实施更多的功能样式。 通过方法参数传递状态。 操作较少的对象状态。

8.短路equals()

这是一个低落的果实。 在大型对象图中,如果所有对象的equals()方法首先比较便宜地比较身份,则可以显着提高性能:

@Override
public boolean equals(Object other) {
    if (this == other) return true;

    // Rest of equality logic...
}

请注意,其他短路检查可能涉及空检查,该检查也应该存在:

@Override
public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null) return false;

    // Rest of equality logic...
}

规则 :短路所有equals()方法以获得性能。

9.尝试使方法默认为final

有些人对此持不同意见,因为默认情况下使事情最终完成与Java开发人员所习惯的相反。 但是,如果您完全控制所有源代码,则默认情况下将方法设为final绝对没有问题,因为:

  • 如果确实需要重写方法(确实吗?),仍然可以删除final关键字
  • 您再也不会意外覆盖任何方法

这特别适用于静态方法,在这些方法中“覆盖”(实际上是阴影)几乎没有任何意义。 最近,我在Apache Tika上遇到了一个非常糟糕的阴影静态方法示例。 考虑:

TikaInputStream扩展了TaggedInputStream并使用完全不同的实现来隐藏其静态get()方法。

与常规方法不同,静态方法不会互相覆盖,因为调用站点在编译时绑定了静态方法调用。 如果您不走运,您可能会偶然得到错误的方法。

规则 :如果您完全控制自己的API,请尝试在默认情况下尽可能多地使用final方法。

10.避免方法(T…)签名

偶尔接受一个Object...参数的“ accept-all” varargs方法没有任何问题:

void acceptAll(Object... all);

编写这样的方法给Java生态系统带来一点JavaScript的感觉。 当然,您可能希望将实际类型限制为在实际情况下更受限的内容,例如String... 而且由于您不想限制太多,您可能会认为用通用T代替Object是一个好主意:

void acceptAll(T... all);

但事实并非如此。 T总是可以推断为Object。 实际上,您最好不要将泛型与上述方法一起使用。 更重要的是,您可能认为可以重载上述方法,但是您不能:

void acceptAll(T... all);
void acceptAll(String message, T... all);

看起来您可以选择将String消息传递给该方法。 但是这里的电话怎么办?

acceptAll("Message", 123, "abc");

编译器会推断<? extends Serializable & Comparable<?>>T <? extends Serializable & Comparable<?>> ,这使调用变得模棱两可!

因此,每当您拥有“所有人都接受”的签名(即使它是通用的)时,您将永远无法再次安全地重载它。 API使用者可能只是幸运地“偶然地”选择了编译器选择“正确的”最具体的方法。 但是他们也可能被欺骗使用“ accept-all”方法,或者根本无法调用任何方法。

规则 :如果可以,请避免“全部接受”签名。 如果不能,则不要重载这种方法。

结论

Java是野兽。 与其他更高级的语言不同,它已经发展到今天。 那可能是一件好事,因为在Java的发展速度下,已经有数百个警告,这些警告只能通过多年的经验来掌握。


翻译自: https://www.javacodegeeks.com/2013/08/10-subtle-best-practices-when-coding-java.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值