[英]When should I use streams?
我剛剛在使用List
及其stream()
方法時遇到了一個問題。 雖然我知道如何使用它們,但我不太確定何時使用它們。
例如,我有一個列表,其中包含到不同位置的各種路徑。 現在,我想檢查一個給定的路徑是否包含列表中指定的任何路徑。 我想根據是否滿足條件返回一個boolean
。
當然,這本身並不是一項艱巨的任務。 但我想知道我應該使用流還是 for(-each) 循環。
列表
private static final List<String> EXCLUDE_PATHS = Arrays.asList(
"my/path/one",
"my/path/two"
);
使用流的示例:
private boolean isExcluded(String path) {
return EXCLUDE_PATHS.stream()
.map(String::toLowerCase)
.filter(path::contains)
.collect(Collectors.toList())
.size() > 0;
}
使用 for-each 循環的示例:
private boolean isExcluded(String path){
for (String excludePath : EXCLUDE_PATHS) {
if (path.contains(excludePath.toLowerCase())) {
return true;
}
}
return false;
}
請注意, path
參數始終為小寫。
我的第一個猜測是 for-each 方法更快,因為如果滿足條件,循環會立即返回。 而流仍會遍歷所有列表條目以完成過濾。
我的假設正確嗎? 如果是這樣,那么為什么(或者更確切地說是什么時候)我會使用stream()
呢?
你的假設是正確的。 您的流實現比 for 循環慢。
這個流的使用應該和 for 循環一樣快:
EXCLUDE_PATHS.stream()
.map(String::toLowerCase)
.anyMatch(path::contains);
這將遍歷項目,將String::toLowerCase
和過濾器逐一應用於項目,並在匹配的第一個項目處終止。
collect()
和anyMatch()
都是終端操作。 anyMatch()
在第一個找到的項目處退出,而collect()
要求處理所有項目。
是否使用 Streams 的決定不應由性能考慮驅動,而是由可讀性驅動。 當談到性能時,還有其他考慮因素。
使用您的.filter(path::contains).collect(Collectors.toList()).size() > 0
方法,您正在處理所有元素並將它們收集到一個臨時List
,然后再比較大小,這幾乎從來沒有對於由兩個元素組成的 Stream 很重要。
如果您有大量元素,使用.map(String::toLowerCase).anyMatch(path::contains)
可以節省 CPU 周期和內存。 盡管如此,這會將每個String
轉換為其小寫表示,直到找到匹配項。 顯然,有一點使用
private static final List<String> EXCLUDE_PATHS =
Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
.collect(Collectors.toList());
private boolean isExcluded(String path) {
return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}
反而。 因此,您不必在每次調用isExcluded
重復轉換為小寫isExcluded
。 如果EXCLUDE_PATHS
的元素數量或字符串的長度變得非常大,您可以考慮使用
private static final List<Predicate<String>> EXCLUDE_PATHS =
Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
.map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
.collect(Collectors.toList());
private boolean isExcluded(String path){
return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}
將字符串編譯為帶有LITERAL
標志的正則表達式模式,使其行為就像普通的字符串操作一樣,但允許引擎花一些時間進行准備,例如使用 Boyer Moore 算法,以便在實際比較時更高效。
當然,這只有在有足夠多的后續測試來補償准備工作所花費的時間時才會有回報。 確定是否會是這種情況是實際性能考慮之一,除了第一個問題之外,此操作是否永遠是性能關鍵。 不是使用 Streams 還是for
循環的問題。
順便說一句,上面的代碼示例保留了原始代碼的邏輯,這在我看來是有問題的。 如果指定的路徑包含列表中的任何元素,則isExcluded
方法返回true
,因此它為/some/prefix/to/my/path/one
以及my/path/one/and/some/suffix
返回true
甚至/some/prefix/to/my/path/one/and/some/suffix
。
甚至dummy/path/onerous
被認為滿足條件,因為它contains
字符串my/path/one
...
是的。 你是對的。 您的流方法會有一些開銷。 但是你可以使用這樣的結構:
private boolean isExcluded(String path) {
return EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}
使用流的主要原因是它們使您的代碼更簡單易讀。
Java 中流的目標是簡化編寫並行代碼的復雜性。 它的靈感來自函數式編程。 串行流只是為了使代碼更清晰。
如果我們想要性能,我們應該使用parallelStream,它的設計目的是。 一般來說,串行的速度較慢。
有一篇關於ForLoop
、 Stream
和ParallelStream
Performance的好文章可供閱讀。
在您的代碼中,我們可以使用終止方法來停止第一個匹配項的搜索。 (任何匹配...)
正如其他人提到的很多優點,但我只想提一下流評估中的惰性評估。 當我們使用map()
創建小寫路徑的流時,我們不會立即創建整個流,而是延遲構造流,這就是為什么性能應該等同於傳統的 for 循環。 它沒有進行全面掃描, map()
和anyMatch()
是同時執行的。 一旦anyMatch()
返回 true,它將被短路。
激進的答案:
從來沒有。 曾經。 曾經。
我幾乎從不為任何事情迭代列表,尤其是為了找到一些東西,但 Stream 用戶和系統似乎充滿了這種編碼方式。
很難重構和組織這樣的代碼,因此冗余和過度迭代無處不在。 用同樣的方法,你可能會看到它 5 次。 同樣的清單,發現不同的東西。
它也不是很短。 很少是。 絕對不是更具可讀性,但這是一個主觀意見。 有些人會說是。 我不。 人們可能會喜歡它,由於自動完成,但在我的IntelliJ的編輯,我可以iter
或itar
,並有針對我創建了類型,一切都環汽車。
誤用和過度使用,最好完全避免。 Java 不是真正的函數式語言,Java 泛型很爛,表達能力不夠強,當然更難閱讀、解析和重構。
流代碼不容易提取或重構,除非您想開始添加返回Optionals
、 Predicates
、 Consumers
奇怪方法,並且最終返回方法並采用各種奇怪的通用約束,其順序和含義只有上帝知道。 對於許多需要訪問方法以找出各種事物類型的推斷。
試圖讓 Java 表現得像Haskell或Lisp這樣的函數式語言是愚蠢的差事。 一個沉重的基於 Streams 的 Java 系統總是比沒有任何系統更復雜,而且性能更低,重構和維護更復雜。 因此也有更多的錯誤和充滿補丁的工作。 到處粘。
當 OpenJDK 參與進來時,他們開始在沒有真正徹底考慮的情況下向語言添加內容。 它不僅僅是Java Streams。 因此,此類系統本質上更復雜,因為它們需要更多的基礎知識。 你可能有,但你的同事沒有。 他們肯定知道 for 循環是什么以及 if 塊是什么。
此外,由於您也不能為非最終變量分配任何內容,因此在循環時您很少可以同時做兩件事,因此您最終會迭代兩次或三次。
大多數喜歡和喜歡 Streams 方法而不是 for 循環的人是在 Java 8 之后開始學習 Java 的人。以前的人討厭它。 問題是它的使用、重構和使用正確的方式要復雜得多。
當我說它的性能更差時,它不是與 for 循環相比,這也是一個非常真實的事情,但更多的是由於此類代碼必須過度迭代各種事物的趨勢。
迭代一個列表以找到它往往會一遍又一遍地完成的項目被認為很容易。
我還沒有看到一個系統從中受益。 我見過的所有系統都實施得很糟糕,主要是因為它。
代碼絕對不比 for 循環更具可讀性,而且在 for 循環中絕對更靈活和可重構。 我們今天在任何地方看到這么多如此復雜的垃圾系統和錯誤的原因是我向您保證,由於嚴重依賴 Streams 進行過濾,更不用說過度使用 Lombok 和 Jackson。 這三個是系統實施不當的標志。 關鍵字過度使用。 補丁工作方法。
同樣,我認為迭代列表以查找任何內容真的很糟糕。 然而,對於基於 Stream 的系統,這就是人們一直在做的事情。 解析和檢測迭代可能是 O(N2) 而使用 for 循環時,您會立即看到它,這也並不罕見和困難。
通常要求數據庫為您過濾事物的習慣並不罕見,基本查詢會返回一大堆事物並使用各種迭代事物和方法進行擴展以涵蓋用例以過濾掉不需要的事物,以及當然,他們使用 Streams 來做到這一點。 圍繞這個大列表出現了各種各樣的方法,其中包含各種要過濾的內容。
再三,一而再再而三。 當然,我不是說你。 你的同事。 對?
我幾乎從不迭代任何東西。 我使用正確的數據集並依靠數據庫為我過濾它。 一次。 但是,在 Streams 重系統中,您會在任何地方看到這一點。 在最深的方法中,在調用者,調用者的調用者,調用者的調用者的調用者中。 處處流水。 它很丑。 祝你好運重構那些存在於微小 lambda 表達式中的代碼。
祝你好運重用它們。 沒有人會希望重用你漂亮的謂詞。 如果他們想使用它們,你猜怎么着。 他們需要使用更多的流。 你只是讓自己上癮並進一步逼迫自己。 現在,您是否建議我開始將所有代碼拆分為微小的 Predicates、Consumer、Function 和 BiFcuntions? 只是為了我可以為 Streams 重用該邏輯?
當然,我在 Javascript 中也同樣討厭它,因為到處都是過度迭代。
您可能會說迭代列表的成本不算什么,但系統復雜性會增加,冗余會增加,因此維護成本和錯誤數量也會增加。 它變成了一種基於補丁和膠水的方法來處理各種事情。 只需添加另一個過濾器並刪除它,而不是以正確的方式編碼。
此外,如果您需要三台服務器來托管所有用戶,我只需一台即可。 因此,這種系統所需的可擴展性比非流重系統更早。 對於小型項目,這是一個非常重要的指標。 你可以說 5000 個並發用戶,我的系統可以處理兩倍或三次。
我的代碼中不需要它,當我負責新項目時,第一條規則是完全禁止使用Streams。
這並不是說它沒有用例,或者它有時可能有用,但與允許它相關的風險遠遠超過收益。
當您開始使用 Streams 時,您實際上是在采用一種全新的編程范式。 系統的整個編程風格會發生變化,這就是我所關心的。
你不想要那種風格。 它並不優於舊式。 尤其是在 Java 上。
以期貨 API為例。 當然,您可以開始編寫所有代碼以返回 Promise 或 Future,但是您真的想要嗎? 這能解決什么嗎? 你的整個系統真的能在任何地方跟進嗎? 這對你來說會更好,還是你只是在嘗試並希望你能在某個時候受益?
有些人在 JavaScript 中過度使用JavaRx和過度使用 promise。 當您真正想要基於未來的東西時,真的很少有案例,並且會感覺到非常多的極端案例,您會發現這些 API 具有某些限制並且您剛剛完成。
您可以構建非常復雜且更易於維護的系統,而無需使用這些廢話。 這就是它的內容。 這與您的愛好項目擴展並成為一個可怕的代碼庫無關。
它是關於構建大型復雜企業系統並確保它們保持一致、一致、可重構和易於維護的最佳方法。
此外,您很少獨自在此類系統上工作。 您很可能與至少 10 人以上的人一起工作,他們都在試驗和過度使用 Streams。 因此,雖然您可能知道如何正確使用它們,但您可以放心,其他 9 種方法不會。
我會給你留下這些精彩的真實代碼示例,還有成千上萬個這樣的:
private List<ViewAccessProfileExternalDto> markHiddenSchedules(SystemId systemId, List<ViewAccessProfileExternalDto> accessProfiles) {
final var accessGroupIds =
accessProfiles.stream()
.flatMap(
ap ->
ap.getAccessPolicies().stream()
.map(po -> po.getAccessGroupId())
.filter(Objects::nonNull)
.map(id -> AccessPointGroupId.of(id, systemId)))
.collect(Collectors.toSet());
final var accessGroupsWithDevices = accessGroupHandler.getAccessGroupsWithDevices(accessGroupIds);
final var groupsWithDevices =
accessGroupIds.stream()
.collect(
Collectors.toMap(
id -> id.idString(),
id -> Optional.ofNullable(accessGroupsWithDevices.getOrDefault(id.idString(), null))));
accessProfiles.forEach(
ap -> {
ap.getAccessPolicies()
.forEach(
policy -> {
if (policy.getAccessGroupId() != null) {
final var onlineSchedules =
policy.getSchedules().stream()
.filter(
s ->
ScheduleType.STANDARD
.name()
.equalsIgnoreCase(s.getScheduleType()));
final var offlineSchedules =
policy.getSchedules().stream()
.filter(s -> ScheduleType.OFFLINE.name().equalsIgnoreCase(s.getScheduleType()));
final var opt = groupsWithDevices.get(policy.getAccessGroupId());
if (opt.isPresent()) {
if (!opt.get().isAnyOnlineDevices()) {
onlineSchedules.forEach(sc -> sc.setHidden(true));
}
if (!opt.get().isAnyNonPulseOfflineDevices()) {
offlineSchedules.forEach(sc -> sc.setHidden(true));
}
} else {
onlineSchedules.forEach(sc -> sc.setHidden(true));
offlineSchedules.forEach(sc -> sc.setHidden(true));
}
}
});
});
return accessProfiles;
}
或這個:
private List<DoorInfo> packageDoorInfos(final CredentialUpdateEvent credentialUpdateEvent, final CredentialScheduleHandler.SchedulesAndCounts schedules) {
// For each accessArea in accessPolicy, find connected doorIds and create DoorInfo from each pair
// of lockId, schemaId
final Map<MsfIdDto, List<CredentialUpdateEvent.Door>> doorsPerAccessArea
= credentialUpdateEvent.doors.stream().collect(Collectors.groupingBy(d -> d.accessAreaId));
final Stream<DoorInfoEntry> distinctDoorInfoEntries = credentialUpdateEvent.accessPolicies.stream()
.flatMap(ap -> {
final List<CredentialUpdateEvent.Door> doors = doorsPerAccessArea.get(ap.accessAreaId);
if (doors == null) {
LOGGER.warn("No doors configured for access area {}. Skipping it", ap.accessAreaId);
return Stream.<DoorInfoEntry>empty();
}
final int scheduleId = schedules.scheduleIdFor(ap.scheduleId);
return doors.stream()
.map(d -> new DoorInfoEntry(d, Integer.parseInt(d.lockId), scheduleId));
})
// no need to write more that one entry per lockId, scheduleId tuple, so make them distinct
.distinct()
// sort em by lockId and the scheduleId, which should enable binary search in lcoks
.sorted(byLockIdAndScheduleId);
// convert to DoorInfo's
return distinctDoorInfoEntries.map(di -> DoorInfo.from(di.lockId,
di.scheduleId,
true,
di.door.toggleActive != null ? di.door.toggleActive : false,
di.door.extendedUnlockTimeActive != null ? di.door.extendedUnlockTimeActive : false))
.collect(Collectors.toList());
}
或這個:
public Result result(List<CredentialHolderExternalDto> credentialHolders, List<Apartment> apartments) {
final var fch = forCredentialHolders(credentialHolders);
final var fap = forApartments(apartments);
return new Result(
fch.entrySet().stream()
.filter(e -> e.getValue().isPresent())
.collect(Collectors.toMap(e -> e.getKey().getId(), e -> e.getValue().get())),
fch.entrySet().stream().filter(e -> e.getValue().isEmpty()).map(e -> e.getKey()).collect(Collectors.toList()),
fap.entrySet().stream()
.filter(e -> e.getValue().isPresent())
.collect(Collectors.toMap(e -> e.getKey().getEntityId().idString(), e -> e.getValue().get())),
fap.entrySet().stream().filter(e -> e.getValue().isEmpty()).map(e -> e.getKey()).collect(Collectors.toList()));
}
private Map<CredentialHolderExternalDto, Optional<ViewAccessProfile>> forCredentialHolders(
List<CredentialHolderExternalDto> credentialHolders) {
final var profileIdsPerCredentialHolder =
credentialHolders.stream()
.collect(
toMap(
ch -> ch,
ch ->
ch.getAccessRightsInformation().stream()
.map(ari -> AccessProfileId.of(ari.getAccessProfileId(), systemIdObject))
.collect(Collectors.toList())));
final var profiles =
viewAccessProfileRepository.get(
profileIdsPerCredentialHolder.values().stream().flatMap(List::stream).distinct().collect(toList()));
final var relevantProfiles =
profiles.stream()
.filter(vap -> vap.getType() == AccessProfile.Type.CREDENTIAL_HOLDER)
.collect(toMap(vap -> vap.getEntityId(), Function.identity()));
final var preferredProfilePerCredentialHolder =
profileIdsPerCredentialHolder.entrySet().stream()
.map(
e -> {
final var optProfile =
e.getValue().stream().map(id -> relevantProfiles.get(id)).filter(Objects::nonNull).findFirst();
return new AbstractMap.SimpleEntry<>(e.getKey(), optProfile);
})
.collect(toMap(e -> e.getKey(), e -> e.getValue()));
return preferredProfilePerCredentialHolder;
}
private Map<Apartment, Optional<ViewAccessProfile>> forApartments(List<Apartment> apartments) {
final var profileIdsPerApartment =
apartments.stream()
.collect(
toMap(
ap -> ap,
ap ->
ap.getAccessProfiles().stream()
.map(p -> AccessProfileId.of(p.getId(), systemIdObject))
.collect(toList())));
final var profiles =
viewAccessProfileRepository.get(
profileIdsPerApartment.values().stream().flatMap(List::stream).distinct().collect(toList()));
final var relevantProfiles =
profiles.stream()
.filter(vap -> vap.getType() == AccessProfile.Type.APARTMENT)
.collect(toMap(vap -> vap.getEntityId(), Function.identity()));
final var preferredProfilePerApartment =
profileIdsPerApartment.entrySet().stream()
.map(
e -> {
final var optProfile =
e.getValue().stream().map(id -> relevantProfiles.get(id)).filter(Objects::nonNull).findFirst();
return new AbstractMap.SimpleEntry<>(e.getKey(), optProfile);
})
.collect(toMap(e -> e.getKey(), e -> e.getValue()));
return preferredProfilePerApartment;
}
或這個:
private BatchResponseExternalDto performOperationCredentialContainers(
List<CredentialContainerV2ExternalDto> containers,
BiFunction<CredentialContainerV2ExternalDto, CredentialContainerV2ExternalDto, CredentialContainerV2ExternalDto> transformation,
Function2<CredentialContainerV2ExternalDto, String, DeliveredMessage> operation) {
SystemId systemId = SystemId.of(containers.stream().findFirst().get().getSystemId());
List<CredentialContainerType> allCredentialContainerTypes = credentialContainerTypeHandler.getAll(systemId);
Map<String, List<BatchEntityResponseExternalDto>> resultMap =
io.vavr.collection.List.ofAll(containers)
.map(container -> Tuple.of(container, getOldCredentialContainerV2Dto(container.getId(), systemId)))
.map(
t ->
t.map(
(newContainer, oldContainer) ->
Tuple.of(
newContainer,
applyTransformation(oldContainer, newContainer, transformation))))
.map(
t ->
t.map2(
tryOfTransformedContainer ->
tryOfTransformedContainer.map(
transformedContainer ->
operation.apply(
transformedContainer,
toFormatId(
allCredentialContainerTypes,
transformedContainer)))))
.collect(getBatchEntityCollector());
BatchResponseExternalDto result = new BatchResponseExternalDto(resultMap.get(SUCCESS), resultMap.get(FAILED));
return result;
}
嘗試重構上述內容。 我挑戰你。 試一試。 一切都是流,無處不在。 這就是 Stream 開發人員所做的,他們做得太過分了,並且沒有簡單的方法來掌握代碼實際在做什么。 這個方法返回什么,這個轉換在做什么,我最終會得到什么。 一切都是推斷出來的。 肯定更難閱讀。
如果你明白這一點,那么你一定是愛因斯坦,但你應該知道不是每個人都像你一樣,這可能是你在不久的將來的系統。
請注意,這並非孤立於這個項目,但我已經看到其中許多與這些結構非常相似。
有一件事是肯定的,可怕的程序員喜歡流。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.