任何一个写JAVA代码的程序员都是一名API设计师!不管是否与他人分享代码,代码都将被本身或是他人使用。所以,全部的JAVA开发者都应该了解一个好的API设计的基本内容。html
一个好的API设计须要严谨的思考和大量的经验。幸运的是,咱们能够从其它聪明的人如Ference Mihaly那里学习到这些。Ference Mihaly的博客启发了我写出这篇JAVA8 API附录。当咱们设计Speedment API时,很是依赖他的清单(我建议全部人都阅读如下他的指南)。java
从一开始就步入正轨是很重要的,由于一旦API发布之后,就等于对使用它的客户发出坚决的承诺。就像Joshua Bloch所说的那样:“公开的API是永恒的,就像钻石同样。你只有一次机会使其成为正确的设计,因此尽己所能”。一个精心设计的API使坚决而准确的承诺以及实现的高度灵活性结合在一块儿,并最终惠及API的的设计者和使用者。程序员
为何要使用清单?设计正确的API(好比,定义一组JAVA类的可见部分)比编写API背后进行真实操做的实现类要困难的多。这是极少数人才能掌握的艺术。使用清单可使开发人员避免最明显的错误,从而成为一个更好的开发人员,并节约了大量的时间。面试
强烈建议API设计师从使用者的视角来优化代码的简洁性,易用性和一致性 - 而不是去考虑API的具体实现。同时,他们应该尽量隐藏实现的细节。数据库
Null
说明值的缺失能够说,不一致的空处理(致使无处不在的NullPointerException
)是JAVA历史上最大的错误来源。一些设计师认为引入null是计算机领域最大的错误。幸运的是,随着Optional
类的出现,在JAVA8中引入了缓解空处理问题的第一步。确保一个可能返回空值的方法用返回Optional代替。segmentfault
这明确的向API用户说明这个方法可能返回值,也可能不返回值。不要试图出于提升性能的缘由使用null而不是optional。JAVA8的分析系统会优化大多数的Optional对象。避免在参数和变量中使用Optional。api
正确的作法数组
public Optional<String> getComment() { return Optional.ofNullable(comment); }
错误的作法微信
public String getComment() { return comment; // comment is nullable }
在JAVA5中引入枚举时,出现了一个重大的API问题。咱们都知道枚举类有一个方法values()
返回该枚举类全部值的数组。如今,由于JAVA框架必须确保客户代码不会修改枚举类的值(好比,直接写入数组),那么每一个value()
方法的调用都会产生内部数组的一个复制数组。数据结构
这样影响了性能,而且下降了客户代码的可用性。若是枚举类返回了一个不可修改的List,这个List能够在每一次调用中被复用,那么客户端就能够得到一个更好更可用的枚举值模型。在一般状况下,若是API要返回一组对象,能够考虑提供一个Stream。这能够说明该值只可读(而不是具备set()
方法的List)。
它还容许客户代码轻松的收集另外一个数据结构中的元素,而且进行及时的处理。不只如此,API可以惰性初始化元素(好比,从文件,端口或是数据库中获取内容)。JAVA8的分析系统会确保在JAVA堆上尽量少的建立对象。
一样,不要使用数组做为传入方法的参数,由于,除非防护性的复制了数组,不然另外一个线程可能在方法执行期间修改了数组的内容。
正确的作法
public Stream<String> comments() { return Stream.of(comments); }
错误的作法
public String[] comments() { return comments; // Exposes the backing array! }
避免客户代码直接选择一个接口的实现类。容许客户代码直接建立实现类形成了API和客户端代码之间更直接的耦合。它还使API的涉及范围更广,由于咱们如今须要维护全部的实现类,使它们和外部观察到的实现彻底一致,而不是面向接口。
能够添加一个静态的接口方法,容许客户代码经过该方法建立实现该接口的实现。好比,若是咱们有个Point
接口,其中有两个方法int x()
和int y()
,而后咱们暴露一个静态方法Point.of(int x, int y)
提供该接口的一个实现。
因此,若是x和y都是0,咱们能够返回一个特殊的实现类PointOrigoImpl
(该类不包含x或是y域),不然咱们能够返回另外一个类PointImpl
,该类包含x和y域而且值被设置为传入值。确保实现类在另外一个包中,而且不是API的一部分(好比将Point
放入com.company. product.shape
,将实现类放入com.company.product.internal.shape
)。
正确的作法
Point point = Point.of(1,2);
错误的作法
Point point = new PointImpl(1,2);
出于某种缘由,对于任何JAVA类,都只能有一个父类。不只如此,在API中暴露抽象类或是基类供客户代码进行继承是一个很麻烦的API承诺。应当完全避免API继承,而且替换为静态的接口,该静态接口能够接收一个或多个lambda参数,而且将这些lambda做用于默认的内部API实现类。
这样的话可以使关注点更好的分离。好比,再也不从一个公开的API类AbstractReader
继承而且重写抽象的方法abstract void handleError(IOException ioe)
,而是在Reader接口中暴露一个静态的方法或是构造器来接收Consumer<IOException>
而且运用于内部的ReaderImpl
。
正确的作法
Reader reader = Reader.builder() .withErrorHandler(IOException::printStackTrace) .build();
错误的作法
Reader reader = new AbstractReader() { @Override public void handleError(IOException ioe) { ioe. printStackTrace(); } };
在接口上添加@FunctionalInterface
注解说明API用户可使用lambda表达式实现该接口。它也确保了随着时间推移,该接口仍然能够用于lambda表达式中,防止抽象方法在之后被意外的添加到API中。
正确作法
@FunctionalInterface public interface CircleSegmentConstructor { CircleSegment apply(Point cntr, Point p, double ang); // abstract methods cannot be added }
错误作法
public interface CircleSegmentConstructor { CircleSegment apply(Point cntr, Point p, double ang); // abstract methods may be accidently added later }
若是有两个或多个相同名称的方法都把功能接口做为参数,这有可能对客户端形成lambda歧义。好比,若是有两个方法add(Function<Point, String> renderer)
和add(Predicate<Point> logCondition)
,而后咱们在客户代码中试图调用point.add(p -> p + "lambda")
,编译器将没法决定使用哪一个方法并报错。所以咱们应当根据特定的用途来对方法命名。
正确作法
public interface Point { addRenderer(Function<Point, String> renderer); addLogCondition(Predicate<Point> logCondition); }
错误作法
public interface Point { add(Function<Point, String> renderer); add(Predicate<Point> logCondition); }
能够很方便的在接口中添加default方法,在某些时候这样作是有意义。好比,一个方法应当对全部类都相同,而且有一个短小且基础的实现,那么它就能够做为接口中的一个默认方法。并且,当接口扩展的时候,有时候为了向后兼容性能够提供默认接口方法。
如咱们所知,功能接口只包含一个抽象方法,因此当必须添加其它方法时,默认方法提供了一个解决方法。可是,应当避免API接口由于没必要要的实现问题而演化为一个实现类。因此若是不知道是否要添加默认实现,能够考虑将方法逻辑移动到一个单独的工具类或是将其放在实现类中。
正确的作法
public interface Line { Point start(); Point end(); int length(); }
错误的作法
public interface Line { Point start(); Point end(); default int length() { int deltaX = start().x() - end().x(); int deltaY = start().y() - end().y(); return (int) Math.sqrt( deltaX * deltaX + deltaY * deltaY ); } }
从历史上看,人们在确保验证方法输入参数方面一直徘徊不前。因此,当以后出现了错误时,出错的真实缘由变得模糊不清,隐藏在一层层的栈踪影中。确保参数在实现类中被使用以前进行空检查,或是符合范围约束,或是任何前序条件。不要试图由于性能缘由跳过参数检查。
JVM会优化并删除冗余的检查,生成高效的代码。使用Objects.requireNonNull()
方法。参数检查也是一种遵循API规定的重要方法。若是API不该当接收空值可是殊不知为什么接收了,用户会感到困惑。
正确的作法
public void addToSegment(Segment segment, Point point) { Objects.requireNonNull(segment); Objects.requireNonNull(point); segment.add(point); }
错误的作法
public void addToSegment(Segment segment, Point point) { segment.add(point); }
Optional.get()
JAVA8的设计者在为Optional.get()
方法命名时犯了个错误,它应当称做Optional.getOrThrow()
或是相似的名字。调用get()
方法却不用Optional.isPresent()
方法检查值是否存在是一个很常见的错误,它严重违背了Optional类的初衷。可使用Optional的其它方法好比map()
,flatMap()
或是ifPresent()
方法来确保在调用get()
以前调用isPresent()
方法。
正确作法
Optional<String> comment = // some Optional value String guiText = comment .map(c -> "Comment: " + c) .orElse("");
错误作法
Optional<String> comment = // some Optional value String guiText = "Comment: " + comment.get();
不管如何,全部的API都会有错。当从API用户那里得到栈跟踪时,将流方法分布在不一样的行每每使问题追踪更加容易。并且会增长代码的可读性:
正确的作法
Stream.of("this", "is", "secret") .map(toGreek()) .map(encrypt()) .collect(joining(" "));
错误的作法
Stream.of("this", "is", "secret").map(toGreek()).map(encrypt()).collect(joining(" "));
使用 Optional 处理 null
Java8 如何正确使用 Optional
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注个人微信公众号!将会不按期的发放福利哦~