آگاهانه از findFirst و findAny استفاده کنید.

بعد از فیلتر کردن یک stream جاوا ۸ بسیار رایج است که از findFirst() یا findAny() برای دریافت المانی که بعد از فیلتر شدن باقیمانده است استفاده کنیم. اما این کاری که شما میخواهید را دقیقا انجام نمیدهد و خطاهای کوچکی ممکن است رخ دهد.
اما مشکل findFirst() و findAny() چیست؟
همانطور که در جاواداک آنها قابل مشاهده است هر دو تابع یک المان دلخواه از استریم را برمیگردانند مگر اینکه استریم ترتیب وقوعی داشته باشد که در آن زمان findFirst() اولین المان را برمیگرداند. به همین سادگی.
یک مثال ساده آن به شکل زیر است:
public Optional<Customer> findCustomer(String customerId) { return customers.stream() .filter(customer -> customer.getId().equals(customerId)) .findFirst(); }
که مشابه نسخه قدیمی حلقه for ان به شکل زیر است:
public Optional<Customer> findCustomer(String customerId) { for (Customer customer : customers) if (customer.getId().equals(customerId)) return Optional.of(customer); return Optional.empty(); }
اما هر دو نمونه یک خطای احتمالی میتوانند داشته باشند: هر دو فرض کردهاند که تنها یک مشتری با ID داده شده وجود دارد.
این فرض ممکن است منطقی باشد. شاید این یک محدودیت شناخته شده است که توسط بخشهای اختصاصی سیستم کنترل میشود. در این صورت به طور کلی درست است.
اما در بسیاری از شرایط اینطور نیست. ممکن است که مشتری از یک منبع خارجی بارگذاری شود که هیچ ضمانتی به یکتا بودن شناسه آن نیست. ممکن است این خطا اجازه دهد دو کتاب با شابک یکسانی وجود داشته باشند. ممکن است جستجو نتایج پیشبینی نشدهای داشته باشد.
اغلب صحت کدها بر مبنای فرضیاتی است که در یک حوزه تنها یک المان یکتا مطابقت پیدا میکند اما هیچ کاری برای اعلان یا اعمال چنین محدودیتی انجام نمیدهند.
بدتر از آن اینکه این اشتباه رفتاری کاملا مبتنی بر داده است که ممکن است در حین تست مخفی بماند. مگر اینکه چنین سناریویی در ذهن داشته باشیم که در آن صورت قبل از تولید محصول نهایی آن را در نظر میگیریم.
حتی بدتر از آن اینکه خیلی ساکت با شکست روبرو میشود! اگر این فرض که تنها یک المان وجود دارد نقض شود، ما به طور مستقیم متوجه آن نمیشویم. به جای آن سیستم رفتار درستی برای مدتی نشان نمیدهد تا زمانی که علت آن کشف شود.
مسلما مشکلی با ذات توابع findFirst() و findAny() وجود ندارد. اما خیلی ساده میتوانند مورد استفاده قرار گیرند که منجر به باگ در منطق مدلسازی دامنه شوند.
شکست زودهنگام
حال بیایید مشکل را برطرف کنیم! فرض کنیم که تقریبا مطمئن هستیم که حداکثر یک المان با خواسته ما مطابقت دارد و میخواهیم در صورت نقض این فرض به سرعت با شکست مواجه شویم. در یک حلقه نیاز به مدیریت وضعیتهای نامناسب خواهیم داشت و برنامه به شکل زیر خواهد بود:
public Optional<Customer> findOnlyCustomer(String customerId) { boolean foundCustomer = false; Customer resultCustomer = null; for (Customer customer : customers) if (customer.getId().equals(customerId)) if (!foundCustomer) { foundCustomer = true; resultCustomer = customer; } else { throw new DuplicateCustomerException(); } return foundCustomer ? Optional.of(resultCustomer) : Optional.empty(); }
حالا استریمها راه بهتری در اختیار ما قرار میدهند. ما میتوانیم از reduce استفاده کنیم که طبق مستندات آن، یک کاهش روی المانهای رشته با استفاده از تابع تجمع انجمنی (associative accumulation function) انجام میدهد و یک Optional برمیگرداند که مقدار کاهش یافته را، در صورت وجود، توصیف میکند. در کد زیر که کاری معادل reduce انجام میدهد، توضیحات داده شده را بهتر میتوانید درک کنید:
boolean foundAny = false; T result = null; for (T element : this stream) { if (!foundAny) { foundAny = true; result = element; } else result = accumulator.apply(result, element); } return foundAny ? Optional.of(result) : Optional.empty();
اما در reduce لازم نیست که به ترتیب اجرا شود.
این مشابه حلقه for بالا نیست؟! چه تصادفی..!
پس همه چیزی که نیاز داریم یک انباشتگر (accumulator) است که خطای مورد نظر را به محض فراخوانی پرتاب میکند:
public Optional<Customer> findOnlyCustomerWithId_manualException(String customerId) { return customers.stream() .filter(customer -> customer.getId().equals(customerId)) .reduce((element, otherElement) -> { throw new DuplicateCustomerException(); }); }
اندکی عجیب به نظر میرسد اما کاری که میخواهیم را انجام میدهد. برای خواناتر کردن آن، آن را در یک استریم utility class قرار میدهیم و نام مناسبی به آن میدهیم.
public static <T> BinaryOperator<T> toOnlyElement() { return toOnlyElementThrowing(IllegalArgumentException::new); } public static <T, E extends RuntimeException> BinaryOperator<T> toOnlyElementThrowing(Supplier<E> exception) { return (element, otherElement) -> { throw exception.get(); }; }
پس میتوانیم به شکل زیر فراخوانی کنیم:
// if a generic exception is fine public Optional<Customer> findOnlyCustomer(String customerId) { return customers.stream() .filter(customer -> customer.getId().equals(customerId)) .reduce(toOnlyElement()); } // if we want a specific exception public Optional<Customer> findOnlyCustomer(String customerId) { return customers.stream() .filter(customer -> customer.getId().equals(customerId)) .reduce(toOnlyElementThrowing(DuplicateCustomerException::new)); }
باید توجه شود که برخلاف findFirst() و findAny() این یک عملگر short-circuit نیست و کل استریم را محقق میسازد. همانطور در صورتی که تنها یک المان باشد. پردازش به محض اینکه المان دوم پیدا شود متوقف میگردد.
نتیجه
ما دیدیم که چرا findFirst() و findAny() برای بیان فرض اینکه تنها یک المان در استریم وجود دارند کافی نیستند. برای بیان این فرض و اطمینان از اینکه در صورت نقض آن کد به سرعت با شکست مواجه میشود، لازم است از reduce(toOnlyElement()) استفاده کنیم.
منبع: