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

2024-05-01 16:18
文章标签 java 最佳 实践 编码 微妙

本文主要是介绍10 个 Java 编码中微妙的最佳实践,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这是10个最佳实践的列表,比你平时在Josh Bloch的《 effective java》中看到的规则更加精妙。和Josh Bloch列出的非常容易学习的、和日常情况息息相关的实践相比,这个列表中提到了一些关于设计API/SPI的实践,虽然不常见,但是存在很大的效率问题。

我在编写和维护jOOQ(一种内部DSL,在java中将SQL模块化)时,碰到了这些问题。作为内部DSL,jOOQ最大限度的挑战了java编译器和泛型,把泛型,变量和重载结合到了一起。这种太宽泛的API是Josh Bloch相当不推荐的。

让我来和你分享这10个java编码中微妙的最佳实践:

小panda
小panda
翻译于 5个月前

0人顶

顶 翻译的不错哦!

1.牢记C++的析构函数

还记得C++中的析构函数吗?不记得了?或许你真的很幸运,因为你再也不必为删除对象后,没有及时释放内存而造成内存泄露进行调试了。我们真的应该感谢Sun和Oracle实现垃圾回收机制。

尽管如此,对于我们来说,析构函数仍然有一个很有趣的特点。它常常会让我们对以和分配内存相反的顺序释放内存的工作模式感到容易理解。同样,在JAVA代码中,当你处理如下类析构函数语法的时候,也要把这个特性牢记在心:

当使用@Before和@After但与注解时

当分配和释放JDBC资源时

当调用父类的方法时

也有其他不同的使用案例。这有一个显示如何实现事件监听的实例:

@Override
public void beforeEvent(EventContext e) {super.beforeEvent(e);// Super code before my code
}@Override
public void afterEvent(EventContext e) {// Super code after my codesuper.afterEvent(e);
}
另外一个哲学家用餐的问题,显示了这有多么的重要。

关于哲学家用餐的问题,请查看链接:http://adit.io/posts/2013-05-11-The-Dining-Philosophers-Problem-With-Ron-Swanson.html

法则:无论何时,当你使用before/after, allocate/free, take/return语法实现逻辑时,仔细想想是否需要反序的使用after/free/return操作。

Legend___
Legend___
翻译于 5个月前

1人顶

顶 翻译的不错哦!

2. 不要相信你早期的SPI演进判断

为使用者提供SPI可以很容易让他们注入自定义行为到你的库/代码。当心你的SPI演进判断可能会迷惑你,让你认为(不)需要附加的参数。当然,不应该过早的添加功能。但是一旦你发布了SPI,一旦你决定遵循语义版本,当你发现你可能在某些情况下需要另外一个参数时,你将真的后悔为SPI添加了一个愚蠢的单参数方法:

interface EventListener {// Badvoid message(String message);
}

如果你也需要消息ID和消息源,怎么办?对于上面的类型,API演进将会阻碍你添加参数。当然,有了Java8,你可以添加一个defender方法,“防御”你早期糟糕的设计决策:

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

注意很不幸defender方法不能为final。

但是比起用数十个方法污染你的SPI,使用一个上下文对象(或参数对象)好很多。

interface MessageContext {String message();Integer id();MessageSource source();
}interface EventListener {// Awesome!void message(MessageContext context);
}

比起EventListner SPI你可以更容易演进MessageContext API,因为很少用户会实现它。

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

备注: 使用特定的MessageResult类型传递结果也是一个好的想法,该类型可以通过构造器API构建。这将会为你的SPI提供更多的SPI演进灵活性。

袁不语
袁不语
翻译于 5个月前

1人顶

顶 翻译的不错哦!

3.避免使用匿名,局部或内部类

Swing程序员通常只要按几下快捷键即可生成成百上千的匿名类。在多数情况下,只要遵循接口、不违法SPI子类型的生命周期(SPI subtype lifecycle),这样做也无妨。

但是不要因为一个简单的原因——它们会保存对外部类的引用,就频繁的使用匿名、局部或者内部类。因为无论它们走到哪,外部类就得跟到哪。例如,在局部类的域外操作不当的话,那么整个对象图就会发生微妙的变化从而可能引起内存泄露。

规则:在编写匿名、局部或内部类前请三思能否将它转化为静态的或普通的顶级类,从而避免方法将它们的对象返回到更外层的域中。

注意:使用双层花括号来初始化简单对象:

new HashMap<String, String>() {{put("1", "a");put("2", "b");
}}
这个方法利用了 JLS §8.6规范里描述的实例初始化方法(initializer)。表面上看起来不错,但实际上不提倡这种做法。因为要是使用完全独立的HashMap对象,那么实例就不会一直保存着外部对象的引用。此外,这也会让类加载器管理更多的类。
NinjaSquid
NinjaSquid
翻译于 5个月前

0人顶

顶 翻译的不错哦!

4. 现在就开始编写SAM!

Java8的脚步近了。伴随着Java8带来了lambda表达式,无论你是否喜欢。尽管你的API使用者可能会喜欢,但是你最好确保他们可以尽可能经常的使用。因此除非你的API接收简单的“标量”类型,比如int、long、String 、Date,否则让你的API尽可能经常的接收SAM。

什么是SAM?SAM是单一抽象方法[类型]。也称为函数接口,很快被注释为@FunctionalInterface。这与规则2很配,EventListener实际上就是一个SAM。最好的SAM只有一个参数,因为这将会进一步简化lambda表达式的编写。设想编写

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

替代

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

设想以SAM的方式用jOOX处理XML:

$(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/函数接口。

备注:有许多关于Java8 lambda表达式和改善的Collections API的有趣的博客:

  • http://blog.informatech.cr/2013/04/10/java-optional-objects/
  • http://blog.informatech.cr/2013/03/25/java-streams-api-preview/
  • http://blog.informatech.cr/2013/03/24/java-streams-preview-vs-net-linq/
  • http://blog.informatech.cr/2013/03/11/java-infinite-streams/

袁不语
袁不语
翻译于 5个月前

1人顶

顶 翻译的不错哦!

5.避免让方法返回null

我曾写过1、2篇关于java NULLs的文章,也讲解过Java8中引入新的Optional类。从学术或实用的角度来看,这些话题还是比较有趣的。

尽管现阶段Null和NullPointerException依然是Java的硬伤,但是你仍可以设计出不会出现任何问题的API。在设计API时,应当尽可能的避免让方法返回null,因为你的用户可能会链式调用方法:

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

从上面代码中可看出,任何一个方法都不应返回null。实际上,在通常情况下使用null会被认为相当的异类。像  jQuery 或 jOOX这样的库在可迭代的对象上已完全的摒弃了null。

Null通常用在延迟初始化中。在许多情况下,在不严重影响性能的条件下,延迟初始化也应该被避免。实际上,如果涉及的数据结构过于庞大,那么就要慎用延迟初始化。

规则:无论何时方法都应避免返回null。null仅用来表示“未初始化”或“不存在”的语义。

NinjaSquid
NinjaSquid
翻译于 5个月前

0人顶

顶 翻译的不错哦!

6.设计API时永远不要返回空(null)数组或List

尽管在一些情况下方法返回值为null是可以的,但是绝不要返回空数组或空集合!请看 java.io.File.list()方法,它是这样设计的:
此方法会返回一个指定目录下所有文件或目录的字符串数组。如果目录为空(empty)那么返回的数组也为空(empty)。如果指定的路径不存在或发生I/O错误,则返回null。

因此,这个方法通常要这样使用:

File directory = // ...if (directory.isDirectory()) {String[] list = directory.list();if (list != null) {for (String file : list) {// ...}}
}
大家觉得null检查有必要吗?大多数I/O操作会产生IOExceptions,但这个方法却只返回了null。Null是无法存放I/O错误信息的。因此这样的设计,有以下3方面的不足:
  • Null无助于发现错误
  • Null无法表明I/O错误是由File实例所对应的路径不正确引起的
  • 每个人都可能会忘记判断null情况

以集合的思维来看待问题的话,那么空的(empty)的数组或集合就是对“不存在”的最佳实现。返回空(null)数组或集合几乎是无任何实际意义的,除非用于延迟初始化。

规则:返回的数组或集合不应为null。


NinjaSquid
NinjaSquid
翻译于 5个月前

0人顶

顶 翻译的不错哦!

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
  • 很难使有状态的资源全局可用,因为状态没有被描述

状态就像盒子中的巧克力

戏剧海报《阿甘正传》,版权1994年由派拉蒙影业公司。保留所有权利。相信上述惯例满足所谓的合理使用。

规则:更多的以函数风格实现。通过方法参数转移状态。极少操作对象状态。

袁不语
袁不语
翻译于 5个月前

0人顶

顶 翻译的不错哦!

8. 短路式 equals()

这是一个比较容易操作的方法。在比较复杂的对象系统中,你可以获得显著的性能提升,只要你在所有对象的equals()方法中首先进行相等判断:

@Override
public boolean equals(Object other) {if (this == other) return true;// 其它相等判断逻辑...
}

注意,其它短路式检查可能涉及到null值检查,所以也应当加进去:

@Override
public boolean equals(Object other) {if (this == other) return true;if (other == null) return false;// Rest of equality logic...
}

规则: 在你所有的equals()方法中使用短路来提升性能。

lwei
lwei
翻译于 5个月前

0人顶

顶 翻译的不错哦!

9. 尽量使方法默认为final

有些人可能不同意这一条,因为使方法默认为final与Java开发者的习惯相违背。但是如果你对代码有完全的掌控,那么使方法默认为final是肯定没错的:

  • 如果你确实需要覆盖(override)一个方法(你真的需要?),你仍然可以移除final关键字
  • 你将永远不会意外地覆盖(override)任何方法

这特别适用于静态方法,在这种情况下“覆盖”(实际上是遮蔽)几乎不起作用。我最近在Apache Tika中遇到了一个很糟糕的遮蔽静态方法的例子。考虑:

  • TaggedInputStream.get(InputStream)
  • TikaInputStream.get(InputStream)

TikaInputStream扩展了TaggedInputStream,以一种相对不同的实现遮蔽了它的静态get()方法。

与常规方法不同,静态方法不能互相覆盖,因为调用的地方在编译时就绑定了静态方法调用。如果你不走运,你可能会意外获得错误的方法。

规则:如果你完全掌控你的API,那么使尽可能多的方法默认为final。

袁不语
袁不语
翻译于 5个月前

0人顶

顶 翻译的不错哦!

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

在特殊场合下使用“accept-all”变量参数方法接收一个Object...参数就没有错的:

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");

编译器将T推断为<? extends Serializable & Comparable<?>>,这将会使调用不明确!

所以无论何时你有一个“accept-all”签名(即使是泛型),你将永远不能类型安全地重载它。API使用者可能仅仅在走运的时候才会让编译器“偶然地”选择“正确的”限定最多的方法。但是也可能使用accept-all方法或者无法调用任何方法。

规则: 如果可能,避免“accept-all”签名。如果不能,不要重载这样的方法。

结论

Java是一个野兽。不像其它更理想主义的语言,它慢慢地演进为今天的样子。这可能是一件好事,因为以Java的开发速度就已经有成百上千个警告,而且这些警告只能通过多年的经验去把握。

敬请期待更多关于这个主题的前十名列表!

这篇关于10 个 Java 编码中微妙的最佳实践的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/952117

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

解决IDEA报错:编码GBK的不可映射字符问题

《解决IDEA报错:编码GBK的不可映射字符问题》:本文主要介绍解决IDEA报错:编码GBK的不可映射字符问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录IDEA报错:编码GBK的不可映射字符终端软件问题描述原因分析解决方案方法1:将命令改为方法2:右下jav

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推