这是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 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操作是否应按相反的顺序执行操作。
2.不要相信您早期的SPI发展判断
向消费者提供SPI是允许他们将自定义行为注入您的库/代码中的简便方法。 不过请注意,您的SPI演变判断可能会欺骗您,使您认为(不需要)该附加参数 。 确实, 不应及早添加任何功能。 但是一旦发布了SPI,并决定遵循语义版本控制 ,当您意识到在某些情况下可能还需要另一个参数时,您会后悔自己在SPI中添加了一个愚蠢的单参数方法:
interface EventListener {// Badvoid message(String message);
}
如果还需要消息ID和消息源怎么办? API的发展将阻止您轻松地将该参数添加到上述类型。 使用Java 8,您可以添加防御者方法来“捍卫”您糟糕的早期设计决策:
interface EventListener {// Baddefault 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接受简单的“标量”类型(例如int
, long
, String
, Date
,否则您的API应尽可能多地接受SAM。
什么是SAM? SAM是单一抽象方法[Type]。 也称为功能接口 ,很快将使用@FunctionalInterface注释进行注释 。 这与规则2配合得很好,其中EventListener实际上是SAM。 最好的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()));}
});
想象一下通过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的一些有趣的博客文章可以在这里找到:
- 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.避免从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非常困难
- 很难使有状态资源在全球范围内可用,因为没有记录状态
阿甘正传的戏剧海报, 派拉蒙影业 ( 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上遇到了一个非常糟糕的阴影静态方法示例。 考虑:
-
TaggedInputStream.get(InputStream)
-
TikaInputStream.get(InputStream)
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