Java API编码的10个最佳实践

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

1.记住C ++析构函数

还记得C ++析构函数吗?没有?然后,您可能会很幸运,因为您无需再调试任何代码,而不会由于删除对象后未释放分配的内存而导致内存泄漏。感谢Sun / Oracle实现垃圾回收!
但是,尽管如此,破坏者还是有一个有趣的特征。通常以相反的顺序释放内存是有意义的。在使用类似析构函数的语义进行操作时,也要在Java中记住这一点:
当使用@Before和@After JUnit批注时
分配时,释放JDBC资源
调用超级方法时
还有其他各种用例。这是一个具体示例,显示了如何实现某些事件侦听器SPI:
1个
2
3
4
5
6
7
8
9
10
11 @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);
}

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

向消费者提供SPI是使他们能够将自定义行为注入您的库/代码中的简便方法。但是请注意,您的SPI演变判断可能会欺骗您,使您认为您(不需要)该附加参数。确实,不应及早添加任何功能。但是一旦发布了SPI,并决定遵循语义版本控制,当您意识到在某些情况下可能还需要另一个参数时,您会后悔为SPI添加了一个愚蠢的单参数方法:
1个
2
3
4 interface EventListener {
// Bad
void message(String message);
}
如果您还需要消息ID和消息源怎么办?API的发展将阻止您轻松地将该参数添加到上述类型。使用Java 8,您可以添加防御者方法来“捍卫”您糟糕的早期设计决策:
1个
2
3
4
5
6
7
8
9
10
11
12 interface EventListener {
// Bad
default void message(String message) {
message(message, null, null);
}
// Better?
void message(
String message,
Integer id,
MessageSource source
);
}
请注意,不幸的是,防御者方法不能设为final。
但是,比使用数十种方法污染SPI更好的方法是,仅为此目的使用上下文对象(或参数对象)。
1个
2
3
4
5
6
7
8
9
10 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子类型生命周期的“麻烦”。
但是,您不应该过于频繁地使用匿名类,局部类或内部类,原因很简单:它们保留对外部实例的引用。并且,如果您不小心,它们会将外部实例拖到任何地方,例如,拖到本地类之外的某个范围。这可能是内存泄漏的主要来源,因为整个对象图会突然以微妙的方式纠缠在一起。
规则:每当编写匿名,本地或内部类时,请检查是否可以使其成为静态类,甚至是常规顶级类。避免将匿名,本地或内部类实例从方法返回到外部作用域。
备注:对于简单对象实例化,围绕双花括号有一些聪明的做法:
1个
2
3
4 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是单一抽象方法[Type]。也称为功能接口,很快将使用@FunctionalInterface注释进行注释。这与规则2配合得很好,其中EventListener实际上是SAM。最好的SAM是具有单个参数的SAM,因为它们将进一步简化lambda的编写。想象写作
1个 listeners.add(c -> System.out.println(c.message()));
代替
1个
2
3
4
5
6 listeners.add(new EventListener() {
@Override
public void message(MessageContext c) {
System.out.println(c.message()));
}
});
想象一下通过jOOX进行的 XML处理,它具有几个SAM:
1个
2
3
4
5
6
7 $(document)
// Find elements with an ID
.find(c -> $©.id() != null)
// Find their child elements
.children(c -> ( c ) . t a g ( ) . e q u a l s ( " o r d e r " ) ) / / P r i n t a l l m a t c h e s . e a c h ( c > S y s t e m . o u t . p r i n t l n ( (c).tag().equals("order")) // Print all matches .each(c -> System.out.println( ©))
规则:与您的API使用者友好,现在已经编写SAM /功能接口。

5.避免从API方法返回null

我曾经写过一两次关于Java的NULL的博客。我还写了关于Java 8的Optional简介的博客。从学术和实践的角度来看,这些都是有趣的话题。
虽然NULL和NullPointerExceptions在Java中可能会持续困扰一段时间,但您仍可以通过设计API来避免用户遇到任何问题。尽可能避免从API方法返回null。您的API使用者应能够在适用的情况下链接方法:
1个 initialise(someArgument).calculate(data).dispatch();
在以上代码段中,所有方法均不应返回null。实际上,使用null的语义(缺少值)通常应该是非常例外的。在诸如jQuery(或jOOX,其Java端口)之类的库中,由于始终在可迭代对象上进行操作,因此完全避免了null 。是否匹配某项与下一个方法调用无关。
由于延迟初始化,通常还会出现空值。在许多情况下,也可以避免延迟初始化,而不会对性能产生任何重大影响。实际上,仅应谨慎使用惰性初始化。如果涉及大型数据结构。
规则:尽可能避免从方法返回null。仅对“未初始化”或“缺少”的语义使用null。

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

虽然在某些情况下从方法返回null可以,但是绝对没有用过返回null数组或null集合的用例!让我们考虑一下可怕的java.io.File.list()方法。它返回:
在此抽象路径名表示的目录中命名文件和目录的字符串数组。如果目录为空,则数组为空。如果此抽象路径名不表示目录,或者发生I / O错误,则返回null。
因此,处理此方法的正确方法是
1个
2
3
4
5
6
7
8
9
10
11 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为例。以下示例从存储过程中获取游标:
1个
2
3
4
5
6
7
8
9
10
11
12 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非常困难
很难使有状态资源在全球范围内可用,因为没有记录状态
;
编写这样的方法给Java生态系统带来一点JavaScript的感觉。当然,您可能希望将实际类型限制为在实际情况下更受限的类型,例如String…。而且由于您不想限制太多,您可能会认为用通用T代替Object是一个好主意:
1个 void acceptAll(T… all);
但事实并非如此。T总是可以推断为Object。实际上,您最好不要将泛型与上述方法一起使用。更重要的是,您可能认为可以重载上述方法,但不能:
1个
2 void acceptAll(T… all);
void acceptAll(String message, T… all);
看起来您可以选择将String消息传递给该方法。但是这里的电话怎么办?
1个 acceptAll(“Message”, 123, “abc”);
编译器将推断<? extends Serializable & Comparable<?>>for T,这使调用变得模棱两可!
因此,每当您拥有一个“所有人都接受”的签名(即使它是通用的)时,您将再也无法安全地重载它。API使用者可能只是幸运地“偶然地”选择了编译器选择“正确的”最具体的方法。但是,它们也可能被欺骗使用“全部接受”方法,或者它们可能根本无法调用任何方法。
规则:如果可以,请避免“全部接受”签名。如果不能,则不要重载这种方法。

结论

Java是野兽。与其他更高级的语言不同,它已经发展到今天。那可能是一件好事,因为在Java的发展速度下,已经有数百项警告,这些警告只能通过多年的经验来掌握。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。在这里插入图片描述

发布了76 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zhaozihao594/article/details/104183214