1、10 个 Java 编码中微妙的最佳实践广州传智播客作为华南地区 Java 与 Android 培训的领头羊,对 Java 与 Android 的研究都是走在互联网发展的潮流最前沿,把最新最好的技术教导给学生。在课程体系外,还有很多细致的知识点分享给大家:这是 10 个最佳实践的列表,比你平时在 Josh Bloch 的 effective java中看到的规则更加精妙。和Josh Bloch 列出的非常容易学习的、和日常情况息息相关的实践相比,这个列表中提到了一些关于设计API/SPI 的实践,虽然不常见,但是存在很大的效率问题。我在编写和维护 jOOQ( 一种内部 DSL,在 java 中
2、将 SQL 模块化)时,碰到了这些问题。作为内部 DSL,jOOQ 最大限度的挑战了 java 编译器和泛型,把泛型,变量和重载结合到了一起。这种太宽泛的 API 是 Josh Bloch 相当不推荐的。让我来和你分享这 10 个 java 编码中微妙的最佳实践:1.牢记 C+的析构函数还记得 C+中的析构函数吗?不记得了?或许你真的很幸运,因为你再也不必为删除对象后,没有及时释放内存而造成内存泄露进行调试了。我们真的应该感谢 Sun 和 Oracle 实现垃圾回收机制。尽管如此,对于我们来说,析构函数仍然有一个很有趣的特点。它常常会让我们对以和分配内存相反的顺序释放内存的工作模式感到容易理解
3、。同样,在 JAVA 代码中,当你处理如下类析构函数语法的时候,也要把这个特性牢记在心:当使用Before 和After 但与注解时当分配和释放 JDBC 资源时当调用父类的方法时也有其他不同的使用案例。这有一个显示如何实现事件监听的实例:?123456Overridepublic void beforeEvent(EventContext e) super.beforeEvent(e); / Super code before my code 7891011Overridepublic void afterEvent(EventContext e) / Super code after my
4、 code super.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 可以很容易让他们注入自定义行为到你的库
5、/代码。当心你的 SPI 演进判断可能会迷惑你,让你认为 (不) 需要附加的参数 。当然,不应该过早的添加功能。但是一旦你发布了 SPI,一旦你决定遵循语义版本,当你发现你可能在某些情况下需要另外一个参数时,你将真的后悔为 SPI 添加了一个愚蠢的单参数方法:?1234interface EventListener / Bad void message(String message); 如果你也需要消息 ID 和消息源,怎么办?对于上面的类型, API 演进将会阻碍你添加参数。当然,有了Java8,你可以添加一个 defender 方法,“防御”你早期糟糕的设计决策:?123456789101
6、112interface EventListener / Bad default void message(String message) message(message, null, null); / Better? void message( String message, Integer id, MessageSource source ); 注意很不幸 defender 方法 不能为 final。但是比起用数十个方法污染你的 SPI,使用一个上下文对象(或参数对象) 好很多。?12345678910interface MessageContext String message(); I
7、nteger id(); MessageSource source(); interface EventListener / Awesome! void message(MessageContext context); 比起 EventListner SPI 你可以更容易演进 MessageContext API,因为很少用户会实现它。规则: 无论何时你指定 SPI 的时候, 考虑使用上下文/参数对象,而不要编写固定参数数量的方法。备注: 使用特定的 MessageResult 类型传递结果也是一个好的想法,该类型可以通过构造器 API 构建。这将会为你的 SPI 提供更多的 SPI 演进灵活
8、性。3.避免使用匿名,局部或内部类Swing 程序员通常只要按几下快捷键即可生成成百上千的匿名类。在多数情况下,只要遵循接口、不违法 SPI 子类型的生命周期(SPI subtype lifecycle),这样做也无妨。但是不要因为一个简单的原因它们会保存对外部类的引用,就频繁的使用匿名、局部或者内部类。因为无论它们走到哪,外部类就得跟到哪。例如,在局部类的域外操作不当的话,那么整个对象图就会发生微妙的变化从而可能引起内存泄露。规则:在编写匿名、局部或内部类前请三思能否将它转化为静态的或普通的顶级类,从而避免方法将它们的对象返回到更外层的域中。注意:使用双层花括号来初始化简单对象:?12new
9、 HashMap() 34 put(“1“, “a“); put(“2“, “b“); 这个方法利用了 JLS 8.6 规范里描述的实例初始化方法(initializer)。表面上看起来不错,但实际上不提倡这种做法。因为要是使用完全独立的 HashMap 对象,那么实例就不会一直保存着外部对象的引用。此外,这也会让类加载器管理更多的类。4. 现在就开始编写 SAM!Java8 的脚步近了。伴随着 Java8 带来了 lambda 表达式,无论你是否喜欢。尽管你的 API 使用者可能会喜欢,但是你最好确保他们可以尽可能经常的使用。因此除非你的 API 接收简单的“ 标量”类型,比如 int、lo
10、ng、String 、Date ,否则让你的 API 尽可能经常的接收 SAM。什么是 SAM?SAM 是单一抽象方法类型 。也称为函数接口 ,很快被注释为FunctionalInterface。这与规则 2 很配,EventListener 实际上就是一个 SAM。最好的 SAM 只有一个参数,因为这将会进一步简化 lambda 表达式的编写。设想编写?1listeners.add(c - System.out.println(c.message();替代?123456listeners.add(new EventListener() Overridepublic void message(
11、MessageContext c) System.out.println(c.message(); );设想以 SAM 的方式用 jOOX 处理 XML:?12$(document) / Find elements with an ID 34567.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 使用者好一点儿,从现在开始编
12、写 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
13、-infinite-streams/5.避免让方法返回 null我曾写过 1、2 篇关于 java NULLs 的文章,也讲解过 Java8 中引入新的 Optional 类。从学术或实用的角度来看,这些话题还是比较有趣的。尽管现阶段 Null 和 NullPointerException 依然是 Java 的硬伤,但是你仍可以设计出不会出现任何问题的 API。在设计 API 时,应当尽可能的避免让方法返回 null,因为你的用户可能会链式调用方法:?1initialise(someArgument).calculate(data).dispatch();从上面代码中可看出,任何一个方法都不应
14、返回 null。实际上,在通常情况下使用 null 会被认为相当的异类。像 jQuery 或 jOOX 这样的库在可迭代的对象上已完全的摒弃了 null。Null 通常用在延迟初始化中。在许多情况下,在不严重影响性能的条件下,延迟初始化也应该被避免。实际上,如果涉及的数据结构过于庞大,那么就要慎用延迟初始化。规则:无论何时方法都应避免返回 null。null 仅用来表示“未初始化 ”或“不存在” 的语义。6.设计 API 时永远不要返回空(null)数组或 List尽管在一些情况下方法返回值为 null 是可以的,但是绝不要返回空数组或空集合!请看 java.io.File.list()方法,
15、它是这样设计的:此方法会返回一个指定目录下所有文件或目录的字符串数组。如果目录为空(empty)那么返回的数组也为空(empty)。如果指定的路径不存在或发生 I/O 错误,则返回 null。因此,这个方法通常要这样使用:?1234567891011File directory = / . if (directory.isDirectory() String list = directory.list(); if (list != null) for (String file : list) / . 大家觉得 null 检查有必要吗?大多数 I/O 操作会产生 IOExceptions,但这个
16、方法却只返回了null。Null 是无法存放 I/O 错误信息的。因此这样的设计,有以下 3 方面的不足: Null 无助于发现错误 Null 无法表明 I/O 错误是由 File 实例所对应的路径不正确引起的 每个人都可能会忘记判断 null 情况 以集合的思维来看待问题的话,那么空的(empty)的数组或集合就是对 “不存在”的最佳实现。返回空(null)数组或集合几乎是无任何实际意义的,除非用于延迟初始化。规则:返回的数组或集合不应为 null。7. 避免状态,使用函数HTTP 的好处是无状态。所有相关的状态在每次请求和响应中转移。这是 REST 命名的本质:表征状态转移。在 Java
17、中这样做也很赞。当方法接收状态参数对象的时候从规则 2 的角度想想这件事。如果状态通过这种对象转移,而不是从外边操作状态,那么事情将会更简单。以 JDBC 为例。下述例子从一个存储的程序中读取一个光标。?123456789101112CallableStatement s = connection.prepareCall(“ ? = . “); / Verbose manipulation of statement state: s.registerOutParameter(1, cursor); s.setString(2, “abc“); s.execute(); ResultSet rs
18、 = s.getObject(1); / Verbose manipulation of result set state: rs.next(); rs.next();这使得 JDBC API 如此的古怪。每个对象都是有状态的,难以操作。具体的说,有两个主要的问题: 在多线程环境很难正确的处理有状态的 API 很难使有状态的资源全局可用,因为状态没有被描述 戏剧海报阿甘正传,版权 1994 年由派拉蒙影业公司。保留所有权利。相信上述惯例满足所谓的合理使用。规则:更多的以函数风格实现。通过方法参数转移状态。极少操作对象状态。8. 短路式 equals()这是一个比较容易操作的方法。在比较复杂的对
19、象系统中,你可以获得显著的性能提升,只要你在所有对象的 equals()方法中首先进行相等判断 :?12345Overridepublic boolean equals(Object other) if (this = other) return true; / 其它相等判断逻辑. 注意,其它短路式检查可能涉及到 null 值检查,所以也应当加进去 :?123456Overridepublic boolean equals(Object other) if (this = other) return true; if (other = null) return false; / Rest of
20、 equality logic. 规则: 在你所有的 equals()方法中使用短路来提升性能。9. 尽量使方法默认为 final有些人可能不同意这一条,因为使方法默认为 final 与 Java 开发者的习惯相违背。但是如果你对代码有完全的掌控,那么使方法默认为 final 是肯定没错的: 如果你确实需要覆盖(override)一个方法(你真的需要?) ,你仍然可以移除 final 关键字 你将永远不会意外地覆盖(override)任何方法 这特别适用于静态方法,在这种情况下“覆盖”(实际上是遮蔽)几乎不起作用。我最近在 Apache Tika 中遇到了一个很糟糕的遮蔽静态方法的例子。考虑:
21、 TaggedInputStream.get(InputStream) TikaInputStream.get(InputStream) TikaInputStream 扩展了 TaggedInputStream,以一种相对不同的实现遮蔽了它的静态 get()方法。与常规方法不同,静态方法不能互相覆盖,因为调用的地方在编译时就绑定了静态方法调用。如果你不走运,你可能会意外获得错误的方法。规则:如果你完全掌控你的 API,那么使尽可能多的方法默认为 final。10. 避免方法 (T)签名在特殊场合下使用“accept-all”变量参数方法接收一个 Object.参数就没有错的:?1void a
22、cceptAll(Object. all);编写这样的方法为 Java 生态系统带来一点儿 JavaScript 的感觉。当然你可能想要根据真实的情形限制实际的类型,比如 String.。因为你不想要限制太多,你可能会认为用泛型 T 取代 Object 是一个好想法:?1void acceptAll(T. all);但是不是。T 总是会被推断为 Object。实际上你可能仅仅认为上述方法中不能使用泛型。更重要的是你可能认为你可以重载上述方法,但是你不能:?12void acceptAll(T. all); void acceptAll(String message, T. all);这看起来好
23、像你可以可选地传递一个 String 消息到方法。但是这个调用会发生什么呢??1acceptAll(“Message“, 123, “abc“);编译器将 T 推断为,这将会使调用不明确!所以无论何时你有一个“accept-all”签名(即使是泛型),你将永远不能类型安全地重载它。API 使用者可能仅仅在走运的时候才会让编译器“偶然地”选择“ 正确的”限定最多的方法。但是也可能使用accept-all 方法或者无法调用任何方法。规则: 如果可能,避免 “accept-all”签名。如果不能,不要重载这样的方法。结论Java 是一个野兽。不像其它更理想主义的语言,它慢慢地演进为今天的样子。这可能
24、是一件好事,因为以 Java 的开发速度就已经有成百上千个警告,而且这些警告只能通过多年的经验去把握。广州传智播客的讲师以朴素的语言,采用由浅入深,先易后难的教学方法,进行全程的项目实训,使学员了解并掌握软件开发的整个项目流程,快速适应企业的人才需求。传智播客培养的软件开发人才受到社会及企业的广泛赞赏和认同,很多学员已成为众多国际国内知名 IT 企业的抢手人才或技术骨干。2010 年 4 月,传智播客广州中心成立;截止 2012 年 8 月,传智播客广州中心已为华南地区的软件相关企业输出近 500 名高级软件工程师;2011 年 10 月,推出了“零付款就读”等不用付一分钱就可以参加就读的项目,满足了广大渴望系统地学习软件开发专业技能地学员需求,深受广大学生欢迎。学软件开发可以来广州传智播客 Java 培训(http:/)学校,广州传智播客的培养模式重点在于学生理论知识和学生实践技能,让学生在实践环节中掌握技能,提高人才的综合素质。在教学方式上,以项目实战带动教学,上课打破传统课堂模式,以实训项目贯穿教学,带领学生一起做企业真实项目。从而全面掌握规范的开发流程和丰富的项目开发经验,让实训学员在实训中就拥有实际的工作经验,学成后轻松就业。