در مقالههای قبلی این مجموعه آموزشی، نحوهی تبدیل حلقههای نوشتهشده به سبک دستوری (Imperative) به سبک تابعی (Functional) را بررسی کردیم. در این مقاله نگاه میکنیم به نحوهی دیدن منبع داده — از دریچهی چشم تابعی — بهعنوان یک Stream از داده و تبدیل حلقه به استفاده از Stream API.
دیدیم چگونه میتوانیم از توابع filter() و map() به ترتیب برای انتخاب و تبدیل داده استفاده کنیم. این عملیات را میتوان در میانهی pipeline تابعی اجرا کرد. در مثالهای مقالههای قبلی از توابعی مثل range() و rangeClosed() برای ساخت Stream از مقادیر در یک بازهی اعداد استفاده کردیم. این روش وقتی خوب کار میکند که روی یک بازهی مشخص از مقادیر حلقه میزنیم. اما اغلب ممکن است با دادههایی سر و کار داشته باشیم که از منابع خارجی میآیند — مثلاً از یک فایل. اگر بتوانیم با منبع خارجی بهصورت Stream کار کنیم، به راحتی میتوانیم عملیات pipeline تابعی را اعمال نماییم. در این مقاله مثالی را بررسی میکنیم که این ایده را نشان میدهد.
فرض کنید میخواهیم روی یک فایل حلقه بزنیم و تعداد خطوطی را که حاوی یک یا چند بار وقوع یک کلمهی مشخص هستند بشماریم. کد آشنا و معمول سبک دستوری برای انجام این کار این است:
//Sample.java
import java.nio.file.*;
public class Sample {
public static void main(String[] args) {
try {
final var filePath = "./Sample.java";
final var wordOfInterest = "public";
try (var reader = Files.newBufferedReader(Path.of(filePath))) {
String line = "";
long count = 0;
while((line = reader.readLine()) != null) {
if(line.contains(wordOfInterest)) {
count++;
}
}
IO.println(String.format("Found %d lines with the word %s", count, wordOfInterest));
}
} catch(Exception ex) {
IO.println("ERROR: " + ex.getMessage());
}
}
}
برای سهولت کار با این مثال، تعداد خطوط حاوی کلمهی «public» را در همان فایل منبعی جستجو میکنیم که خود کد در آن قرار دارد. میتوانید مقدار filePath را به فایل دیگری تغییر دهید و/یا مقدار wordOfInterest را به چیز دیگری تنظیم کنید، اگر مایل باشید.
در این مثال دو بخش اصلی وجود دارد. از BufferedReader که توسط متد newBufferedReader() برگردانده شده استفاده میکنیم تا به محتوای فایلی که میخواهیم بررسی کنیم دسترسی پیدا کنیم. سپس در حلقهی while هر خط را بررسی میکنیم تا ببینیم کلمهی مورد نظر را دارد یا نه، و اگر داشت، count را یکی زیاد میکنیم تا نشان دهیم خط دیگری با آن کلمه پیدا کردهایم. بیایید این دو بخش را با هم بررسی کنیم — ابتدا بخش دوم.
با نگاه دقیقتر به حلقه و با توجه به بحثهای مقالههای قبلی، میتوانیم تشخیص دهیم که وجود if نشانهی آن است که میتوانیم از عملیات filter() استفاده کنیم — به شرطی که بتوانیم کد را بهصورت pipeline تابعی بنویسیم. وقتی خطوط حاوی کلمهی مورد نظر را فیلتر یا انتخاب کردیم، میتوانیم تعداد آنها را با استفاده از متد count() از Stream بشماریم. احتمالاً کنجکاو هستید و میخواهید بپرسید: «اما Stream کجاست؟» برای پاسخ به این سؤال، بخش اول کد را بررسی میکنیم.
داده — یعنی خطوط متن — از فایلی میآید که مسیرش در متغیر filePath تعریف شده است. ما داده را با استفاده از متد readLine() از BufferedReader و سبک دستوری برای حلقهزدن روی هر خط متن میخوانیم. برای استفاده از pipeline تابعی با عملیاتی مثل filter() به یک Stream از داده نیاز داریم. بنابراین سؤال این است: «آیا امکان دریافت Streamای از داده برای محتوای یک فایل وجود دارد؟»
خوشبختانه پاسخ یک بلهی قاطع است. توسعهدهندگان پشت JDK و زبان جاوا صرفاً قابلیت برنامهنویسی تابعی را معرفی نکردند و نگفتند «موفق باشید». آنها زحمت بهبود JDK و اضافه کردن توابعی را کشیدند تا ما بهعنوان برنامهنویسان بتوانیم از قابلیتهای تابعی جاوا برای کارهای روزمرهمان بهرهی کافی ببریم.
یک راه آسان برای تبدیل محتوای فایل به Stream از داده، استفاده از متد lines() از کلاس Files است که بخشی از پکیج java.nio.file میباشد. بیایید کد سبک دستوری قبلی را با کمک متد lines() — که Streamای از محتوای فایل به ما میدهد — به سبک تابعی بازنویسی کنیم، به این صورت:
//Sample.java
import java.nio.file.*;
public class Sample {
public static void main(String[] args) {
try {
final var filePath = "./Sample.java";
final var wordOfInterest = "public";
try(var stream = Files.lines(Path.of(filePath))) {
long count = stream.filter(line -> line.contains(wordOfInterest))
.count();
IO.println(String.format("Found %d lines with the word %s", count, wordOfInterest));
}
} catch(Exception ex) {
IO.println("ERROR: " + ex.getMessage());
}
}
}
متد lines() نه تنها Streamای از داده روی محتوای فایل فراهم میکند، بلکه بسیاری از حواشی اضافی اطراف خواندن خطوط را حذف میکند. بهجای iterator خارجی (External Iterator) که در آن خطوط را یکییکی دریافت میکردیم، Stream امکان استفاده از iterator داخلی (Internal Iterator) را فراهم میسازد — جایی که میتوانیم روی کاری که باید برای هر خط متن انجام شود تمرکز کنیم، آنگاه که در pipeline Stream ظاهر میشود.
هر وقت با مجموعهای از داده از یک منبع خارجی کار میکنید، این سؤال را از خودتان بپرسید: آیا راهی برای دریافت Streamای از داده روی محتوای آن منبع وجود دارد؟ به احتمال زیاد تابعی برای این کار در JDK یا یک کتابخانهی شخص ثالث (Third-party Library) پیدا خواهید کرد. وقتی Stream را دریافت کردید، میتوانید از عملگرهای تابعی قدرتمندی مثل filter()، map() و... برای حلقهزدن روان روی مجموعهی دادهی بخشی از آن منبع استفاده کنید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/