
了解 API 技術(shù):REST、GraphQL 和異步 API 的比較分析
return Optional.ofNullable(comment);
}
不要這樣做:
public String getComment() {
return comment; // comment is nullable
}
在 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!
}
避免讓客戶端代碼直接指定接口的實(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ù) x
和 y
都為零,可以返回一個(gè)特殊的實(shí)現(xiàn)類 PointOrigoImpl
(不包含 x
或 y
字段)。如果 x
和 y
不為零,則返回另一個(gè)類 PointImpl
,該類保存給定的 x
和 y
值。同時(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 表達(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();
}
};
使用 @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ì)意外添加抽象方法
}
當(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)方法可以輕松添加到接口中,并且在某些情況下這樣做是合理的。例如,當(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);
}
}
在歷史上,確保驗(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);
}
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();
最終,所有 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(" "));
對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力
一鍵對(duì)比試用API 限時(shí)免費(fèi)