return Optional.ofNullable(comment);
}

不要這樣做:

public String getComment() {
return comment; // comment is nullable
}

請(qǐng)勿使用數(shù)組將值傳入 API 和從 API 傳遞值

在 Java 5 中引入 Enum 概念時(shí),確實(shí)存在一個(gè)顯著的 API 設(shè)計(jì)缺陷。Enum 類中的 values() 方法返回一個(gè)包含所有枚舉值的數(shù)組。為了確保客戶端代碼不能通過直接修改數(shù)組來更改枚舉的值,Java 框架每次調(diào)用 values() 方法時(shí)都必須生成一個(gè)內(nèi)部數(shù)組的副本。

這種設(shè)計(jì)的缺陷在于它不僅導(dǎo)致性能下降,還影響了客戶端代碼的可用性。如果 values() 方法返回的是一個(gè)不可修改的 List,則這個(gè) List 可以在每次調(diào)用中重用,使得客戶端代碼能夠訪問更好、更有用的枚舉值模型。

在設(shè)計(jì) API 時(shí),如果需要返回一個(gè)元素集合,建議使用 Stream 而不是數(shù)組。Stream 的使用能夠明確表示結(jié)果是只讀的(不同于具有 set() 方法的 List),并且它允許客戶端代碼更方便地將元素收集到其他數(shù)據(jù)結(jié)構(gòu)中,或即時(shí)對(duì)這些元素進(jìn)行操作。

此外,API 還可以在元素可用時(shí)延遲生成它們,例如從文件、套接字或數(shù)據(jù)庫(kù)中逐步拉取數(shù)據(jù)。這樣不僅能提高代碼的靈活性,還能減少內(nèi)存消耗,尤其是通過 Java 8 中改進(jìn)的逃逸分析,確保在 Java 堆上實(shí)際創(chuàng)建的對(duì)象最少。

同樣重要的是,避免在方法的輸入?yún)?shù)中使用數(shù)組。除非對(duì)數(shù)組進(jìn)行防御性復(fù)制,否則在方法執(zhí)行期間,其他線程有可能修改數(shù)組的內(nèi)容,這會(huì)帶來潛在的線程安全問題。

通過采用不可修改的集合或 Stream 作為返回值和輸入?yún)?shù),API 設(shè)計(jì)能夠提升性能、安全性和可用性,使得客戶端代碼更為簡(jiǎn)潔、可靠。

這樣做:

public Stream<String> comments() {
return Stream.of(comments);
}

不要這樣做:

public String[] comments() {
return comments; // Exposes the backing array!
}

請(qǐng)考慮添加靜態(tài)接口方法,為對(duì)象創(chuàng)建提供單一入口點(diǎn)

避免讓客戶端代碼直接指定接口的實(shí)現(xiàn)類。這種做法會(huì)導(dǎo)致API 與客戶端代碼之間的耦合度增加,并且增加了API 的維護(hù)負(fù)擔(dān),因?yàn)楝F(xiàn)在需要維護(hù)所有可能被外部訪問的實(shí)現(xiàn)類,而不僅僅是接口本身。

考慮引入靜態(tài)接口方法,以便客戶端代碼能夠創(chuàng)建接口的實(shí)現(xiàn)對(duì)象,可能是經(jīng)過專門化處理的。例如,假設(shè)存在一個(gè)接口 Point,它包含兩個(gè)方法:int x()int y()。我們可以提供一個(gè)公開的靜態(tài)方法 Point.of(int x, int y),用以生成接口的實(shí)現(xiàn)實(shí)例。

如果參數(shù) xy 都為零,可以返回一個(gè)特殊的實(shí)現(xiàn)類 PointOrigoImpl(不包含 xy 字段)。如果 xy 不為零,則返回另一個(gè)類 PointImpl,該類保存給定的 xy 值。同時(shí),確保實(shí)現(xiàn)類位于一個(gè)明顯不屬于API 一部分的包中,例如將 Point 接口放在 com.company.product 包中,而其實(shí)現(xiàn)類放在 com.company.product.internal.shape 包中。

這樣做:

Point point = Point.of(1, 2);

不要這樣做:

Point point = new PointImpl(1, 2);

使用函數(shù)接口和lambda的組合

相較于繼承,更推薦使用函數(shù)接口和 Lambda 表達(dá)式的組合。Java 語言規(guī)定,任何給定的類只能有一個(gè)超類。此外,在 API 中公開需要由客戶端代碼繼承的抽象類或基類,這將是一個(gè)重大且具有潛在問題的 API 設(shè)計(jì)承諾。因此,應(yīng)完全避免在 API 中使用繼承,而是考慮提供靜態(tài)接口方法,這些方法接受一個(gè)或多個(gè) Lambda 參數(shù),并將這些給定的 Lambda 應(yīng)用于默認(rèn)的內(nèi)部 API 實(shí)現(xiàn)類。

這種做法也有助于實(shí)現(xiàn)更清晰的關(guān)注點(diǎn)分離。例如,與其從公共 API 類 AbstractReader 繼承并重寫抽象方法 void handleError(IOException ioe),不如在 Reader 接口中公開靜態(tài)方法或構(gòu)建器,該方法接受 Consumer 并將其應(yīng)用于內(nèi)部的泛型 ReaderImpl

這樣做:

Reader reader = Reader.builder()
.withErrorHandler(IOException::printStackTrace)
.build();

不要這樣做:

Reader reader = new AbstractReader() {
@Override
public void handleError(IOException ioe) {
ioe.printStackTrace();
}
};

確保在函數(shù)接口中添加了@FunctionalInterface注釋

使用 @FunctionalInterface 注解標(biāo)記接口,可以向 API 用戶表明他們可以使用 Lambda 表達(dá)式來實(shí)現(xiàn)該接口。此外,通過防止在后續(xù)開發(fā)中意外地向接口中添加抽象方法,它還可以確保接口在一段時(shí)間內(nèi)對(duì) Lambda 表達(dá)式保持可用性。

這樣做:

@FunctionalInterface
public interface CircleSegmentConstructor {
CircleSegment apply(Point cntr, Point p, double ang);
// 不能再添加抽象方法
}

不要這樣做:

public interface CircleSegmentConstructor {
CircleSegment apply(Point cntr, Point p, double ang);
// 后續(xù)可能會(huì)意外添加抽象方法
}

避免重載函數(shù)接口作為參數(shù)的方法

當(dāng)有兩個(gè)或更多具有相同名稱的函數(shù)且它們以函數(shù)接口作為參數(shù)時(shí),這可能會(huì)導(dǎo)致客戶端代碼中的 Lambda 表達(dá)式產(chǎn)生歧義。例如,如果有兩個(gè) Point 方法 add(Function renderer)add(Predicate logCondition),當(dāng)在客戶端代碼中嘗試調(diào)用 Point.add(p -> p + " lambda") 時(shí),編譯器無法確定應(yīng)該調(diào)用哪個(gè)方法,并因此產(chǎn)生錯(cuò)誤。為了避免這種情況,應(yīng)該根據(jù)特定的用途對(duì)方法進(jìn)行命名。

這樣做:

public interface Point {
addRenderer(Function<Point, String> renderer);
addLogCondition(Predicate<Point> logCondition);
}

不要這樣做:

public interface Point {
add(Function<Point, String> renderer);
add(Predicate<Point> logCondition);
}

避免在接口中過度使用默認(rèn)方法

默認(rèn)方法可以輕松添加到接口中,并且在某些情況下這樣做是合理的。例如,當(dāng)某些方法對(duì)于所有實(shí)現(xiàn)類都是相同的、功能簡(jiǎn)單且基礎(chǔ)時(shí),默認(rèn)實(shí)現(xiàn)是一個(gè)可行的選擇。此外,在擴(kuò)展 API 時(shí),為了向后兼容,提供默認(rèn)接口方法有時(shí)也是必要的。

眾所周知,函數(shù)式接口只包含一個(gè)抽象方法,因此在需要添加其他方法時(shí),默認(rèn)方法提供了一個(gè)解決方案。然而,應(yīng)避免讓 API 接口因不必要的實(shí)現(xiàn)細(xì)節(jié)而變得復(fù)雜,進(jìn)而演變?yōu)閷?shí)現(xiàn)類。如果有疑問,可以考慮將方法邏輯移至單獨(dú)的實(shí)用程序類,或?qū)⑵浞湃雽?shí)現(xiàn)類中。

這樣做:

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

確保API方法在被操作之前檢查參數(shù)不變性

在歷史上,確保驗(yàn)證方法輸入?yún)?shù)的工作往往被忽視,這導(dǎo)致當(dāng)錯(cuò)誤發(fā)生時(shí),問題的根本原因常常被掩蓋在堆棧跟蹤的深處。因此,在使用實(shí)現(xiàn)類中的參數(shù)之前,應(yīng)該檢查參數(shù)是否為空,并確保符合任何有效的范圍約束或前提條件。不要因?yàn)樾阅茉蚨^這些參數(shù)檢查。

JVM 能夠優(yōu)化冗余檢查并生成高效的代碼,建議使用 Objects.requireNonNull() 方法。參數(shù)檢查也是確保 API 契約的重要手段。如果 API 不應(yīng)該接受空值但卻沒有進(jìn)行驗(yàn)證,用戶將會(huì)感到困惑。

這樣做:

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

不要簡(jiǎn)單地調(diào)用Optional.get()

Java 8 的 API 設(shè)計(jì)者在命名 Optional.get() 方法時(shí)犯了一個(gè)錯(cuò)誤,它實(shí)際上應(yīng)該被命名為 Optional.getOrThrow() 或類似的名稱。因?yàn)檎{(diào)用 get() 而不先檢查值是否存在于 Optional.isPresent() 方法中,是一個(gè)非常常見的錯(cuò)誤,這種錯(cuò)誤完全否定了 Optional 最初承諾的消除 null 的功能。

在 API 的實(shí)現(xiàn)類中,應(yīng)盡量使用 Optional 的其他方法,如 map()flatMap()ifPresent(),或者確保在調(diào)用 get() 方法之前先調(diào)用 isPresent() 方法進(jìn)行檢查。

這樣做:

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

在實(shí)現(xiàn)API類時(shí),考慮在不同的行上分離流管道

最終,所有 API 都不可避免地會(huì)包含錯(cuò)誤。當(dāng) API 用戶提供堆棧跟蹤時(shí),如果流管道被分成多行,相比于單行表示的流管道,通常更容易確定錯(cuò)誤的實(shí)際原因。同時(shí),這也提高了代碼的可讀性。

這樣做:

Stream.of("this", "is", "secret") 
.map(toGreek())
.map(encrypt())
.collect(joining(" "));

不要這樣做:

Stream.of("this", "is", "secret").map(toGreek()).map(encrypt()).collect(joining(" "));

原文鏈接:API Design With Java 8

上一篇:

API 設(shè)計(jì)詳解

下一篇:

了解 API 技術(shù):REST、GraphQL 和異步 API 的比較分析
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)