- عملکرد بالا برای پذیرش برنامه ها بسیار مهم است و می تواند با پردازش مجموعه داده های بزرگ یا متفاوت تأثیر منفی بگذارد.
- توسعه دهندگان جاوا باید بدانند که چگونه از امکانات داخلی مانند مجموعه ها برای بهینه سازی پردازش و عملکرد داده ها استفاده کنند.
- مجموعهها و استریم های جاوا دو ابزار اساسی برای بهبود عملکرد برنامهها هستند.
- توسعه دهندگان باید در نظر داشته باشند که چگونه روش های مختلف پردازش جریان موازی می توانند بر عملکرد برنامه تأثیر بگذارند.
- بکارگیری استراتژی درست پردازش جریان موازی در مجموعه ها می تواند تفاوت بین افزایش پذیرش و از دست دادن مشتریان باشد.
امروزه برنامه نویسی شامل کار با مجموعه داده های بزرگ است که اغلب شامل انواع مختلفی از داده ها می شود. دستکاری این مجموعه داده ها می تواند کار پیچیده و خسته کننده ای باشد. برای سهولت کار برنامه نویس، جاوا مجموعه های چارچوب مجموعه های Java را در سال 1998 معرفی کرد.
این مقاله در مورد هدف پشت چارچوب مجموعههای جاوا، نحوه کار مجموعههای جاوا و اینکه چگونه توسعهدهندگان و برنامهنویسان میتوانند از مجموعههای جاوا به بهترین نحو استفاده کنند، بحث میکند.
مجموعه جاوا چیست؟
اگرچه جاوا از سن 25 سالگی گذشته است، اما امروزه یکی از محبوبترین زبانهای برنامهنویسی است. بیش از 1000000 وب سایت به شکل های مختلف از جاوا استفاده می کنند و بیش از یک سوم توسعه دهندگان نرم افزار ، جاوا را در جعبه ابزار خود دارند.
جاوا در طول زندگی خود دستخوش تحولات اساسی شده است. یکی از پیشرفتهای اولیه در سال 1998 زمانی که جاوا چارچوب مجموعه (JCF) را معرفی کرد، که کار با اشیاء جاوا را ساده میکرد، رخ داد. JCF یک رابط استاندارد و روشهای رایج برای مجموعهها ارائه کرد، تلاش برنامهنویسی را کاهش داد و سرعت برنامههای جاوا را افزایش داد.
درک تمایز بین مجموعه های جاوا و چارچوب مجموعه های جاوا ضروری است. مجموعههای جاوا صرفاً ساختارهای دادهای هستند که گروهی از اشیاء جاوا را نشان میدهند. توسعهدهندگان میتوانند با مجموعهها به همان روشی که با انواع دادههای دیگر کار میکنند، کار کنند و کارهای رایجی مانند جستجو یا دستکاری محتوای مجموعه را انجام دهند.
نمونه ای از مجموعه در جاوا، رابط مجموعه set(java.util.Set)
است. set مجموعه ای است که اجازه عناصر تکراری را نمی دهد و عناصر را به ترتیب خاصی ذخیره نمی کند. رابط Set متدهای خود را از Collection (java.util.Collection
) به ارث می برد و فقط شامل آن متدها می شود.
علاوه بر مجموعه ها، صف ها (java.util.Queue
) و نقشه ها (java.util.Map
) وجود دارد. نقشهها به معنای واقعی مجموعه نیستند، زیرا رابطهای مجموعه را گسترش نمیدهند، اما توسعهدهندگان میتوانند نقشهها را طوری دستکاری کنند که انگار مجموعه هستند. مجموعهها، صفها، فهرستها و نقشهها هر کدام دارای فرزندانی هستند، مانند مجموعههای مرتب شده (java.util.SortedSet
) و نقشههای قابل پیمایش (java.util.NavigableMap
).
در کار با مجموعه ها، توسعه دهندگان باید با برخی از اصطلاحات خاص مرتبط با مجموعه آشنا باشند و آن ها را درک کنند:
- قابل تغییر در مقابل غیرقابل تغییر: همانطور که این عبارات نشان می دهد، مجموعه های مختلف ممکن است از عملیات اصلاح پشتیبانی کنند یا نکنند.
- قابل تغییر در مقابل غیرقابل تغییر: مجموعه های تغییرناپذیر پس از ایجاد قابل تغییر نیستند. در حالی که موقعیتهایی وجود دارد که مجموعههای غیرقابل تغییر ممکن است به دلیل دسترسی توسط کدهای دیگر همچنان تغییر کنند، مجموعههای تغییرناپذیر از چنین تغییراتی جلوگیری میکنند. مجموعههایی که میتوانند تضمین کنند که هیچ تغییری با اشیاء مجموعه قابل مشاهده نیست، تغییرناپذیر هستند، در حالی که مجموعههای غیرقابل تغییر مجموعههایی هستند که اجازه عملیات اصلاحی مانند «افزودن» یا «پاک کردن» را نمیدهند.
- اندازه ثابت در مقابل اندازه متغیر: این عبارات فقط به اندازه مجموعه اشاره دارند و هیچ نشانه ای از قابل تغییر یا تغییر بودن مجموعه ندارند.
- دسترسی تصادفی در مقابل دسترسی متوالی: اگر مجموعه ای امکان نمایه سازی عناصر منفرد را فراهم کند، دسترسی تصادفی است. در مجموعههای دسترسی متوالی، برای رسیدن به یک عنصر معین، باید در تمام عناصر قبلی پیشرفت کنید. گسترش مجموعههای دسترسی متوالی میتواند آسانتر باشد، اما جستجو به زمان بیشتری نیاز دارد.
برای برنامه نویسان مبتدی ممکن است درک تفاوت بین مجموعه های غیرقابل تغییر و تغییر ناپذیر دشوار باشد. مجموعه های غیر قابل تغییر لزوما تغییر ناپذیر نیستند. در واقع، مجموعههای غیرقابل تغییر اغلب بستهبندیهایی در اطراف یک مجموعه قابل تغییر هستند که کدهای دیگر همچنان میتوانند به آن دسترسی داشته باشند و آنها را تغییر دهند. کدهای دیگر ممکن است واقعاً بتوانند مجموعه زیربنایی را تغییر دهند. کار با مجموعه ها مدتی طول می کشد تا درجه ای از راحتی را با مجموعه های تغییرناپذیر و تغییرناپذیر به دست آورید.
به عنوان مثال، ایجاد یک لیست قابل تغییر از پنج ارز دیجیتال برتر بر اساس ارزش بازار را در نظر بگیرید. شما می توانید با استفاده از متد ()java.util.Collections.unmodifiableList
یک نسخه غیرقابل تغییر از لیست قابل تغییر اساسی ایجاد کنید. همچنان میتوانید فهرست زیربنایی را که در فهرست غیرقابل تغییر ظاهر میشود، تغییر دهید. اما شما نمی توانید به طور مستقیم نسخه غیر قابل تغییر را تغییر دهید.
import java.util.*; public class UnmodifiableCryptoListExample { public static void main(String[] args) { List<String> cryptoList = new ArrayList<>(); Collections.addAll(cryptoList, "BTC", "ETH", "USDT", "USDC", "BNB"); List<String> unmodifiableCryptoList = Collections.unmodifiableList(cryptoList); System.out.println("Unmodifiable crypto List: " + unmodifiableCryptoList); // try to add one more cryptocurrency to modifiable list and show in unmodifiable list cryptoList.add("BUSD"); System.out.println("New unmodifiable crypto List with new element:" + unmodifiableCryptoList); // try to add one more cryptocurrency to unmodifiable list and show in unmodifiable list - unmodifiableCryptoList.add would throw an uncaught exception and the println would not run. unmodifiableCryptoList.add("XRP"); System.out.println("New unmodifiable crypto List with new element:" + unmodifiableCryptoList); } }
در زمان اجرا، خواهید دید که یک افزونه به لیست قابل تغییر اساسی به عنوان اصلاحی از فهرست غیرقابل تغییر نشان داده می شود.
با این حال، اگر یک لیست غیرقابل تغییر ایجاد کنید و سپس سعی کنید لیست اصلی را تغییر دهید، به تفاوت توجه کنید. راه های زیادی برای ایجاد لیست های تغییرناپذیر از لیست های قابل تغییر موجود وجود دارد و در زیر از متد List.copyOf()
استفاده می کنیم.
import java.util.*; public class UnmodifiableCryptoListExample { public static void main(String[] args) { List<String> cryptoList = new ArrayList<>(); Collections.addAll(cryptoList, "BTC", "ETH", "USDT", "USDC", "BNB"); List immutableCryptoList = List.copyOf(cryptoList); System.out.println("Underlying crypto list:" + cryptoList) System.out.println("Immutable crypto ist: " + immutableCryptoList); // try to add one more cryptocurrency to modifiable list and show immutable does not display change cryptoList.add("BUSD"); System.out.println("New underlying list:" + cryptoList); System.out.println("New immutable crypto List:" + immutableCryptoList); // try to add one more cryptocurrency to unmodifiable list and show in unmodifiable list - immutableCryptoList.add("XRP"); System.out.println("New unmodifiable crypto List with new element:" + immutableCryptoList); } }
پس از اصلاح لیست زیرساختی، لیست تغییرناپذیر تغییر را نمایش نمی دهد. و تلاش برای اصلاح لیست تغییرناپذیر مستقیماً منجر به UnsupportedOperationException می شود:
مجموعه ها چگونه با چارچوب مجموعه های جاوا مرتبط هستند؟
قبل از معرفی JCF، توسعه دهندگان می توانستند اشیاء را با استفاده از چندین کلاس خاص، یعنی آرایه، بردار و کلاس های hashtable گروه بندی کنند. متاسفانه این کلاس ها دارای محدودیت های قابل توجهی بودند. علاوه بر نداشتن یک رابط مشترک، گسترش آنها دشوار بود.
- JCFیک معماری مشترک فراگیر برای کار با مجموعه ها ارائه کرد. رابط مجموعه ها شامل چندین مؤلفه مختلف است، از جمله:
- رابط های مشترک: نمایش انواع مجموعه های اولیه، از جمله مجموعه ها، فهرست ها و نقشه ها.
- پیاده سازی ها: پیاده سازی های خاص رابط های مجموعه، اعم از همه منظوره، هدف ویژه و انتزاعی. علاوه بر این، پیادهسازیهای قدیمی مربوط به آرایههای قدیمیتر، کلاسهای vector و hashtable نیز وجود دارد.
- الگوریتمها: روشهای ثابت برای دستکاری مجموعهها
- زیرساخت: پشتیبانی زیربنایی برای واسط های مجموعه های مختلف
JCF در مقایسه با روش های گروه بندی اشیاء قبلی، مزایای بسیاری را به توسعه دهندگان ارائه داد. قابل ذکر است که JCF با کاهش نیاز توسعه دهندگان به نوشتن ساختارهای داده خود، برنامه نویسی جاوا را کارآمدتر کرده است.
اما JCF همچنین اساساً نحوه کار توسعه دهندگان با APIها را تغییر داد. با زبان مشترک جدید برای برخورد با API های مختلف، JCF یادگیری و طراحی API و پیاده سازی آنها را برای توسعه دهندگان ساده تر کرد. علاوه بر این، APIها به مراتب قابلیت همکاری بیشتری پیدا کردند. به عنوان مثال Eclipse Collections، یک کتابخانه مجموعه های جاوا منبع باز است که کاملاً با انواع مختلف مجموعه های جاوا سازگار است.
کارایی توسعه اضافی به وجود آمد، زیرا JCF ساختارهایی را ارائه کرد که استفاده مجدد از کد را بسیار آسان تر می کرد. در نتیجه زمان توسعه کاهش یافت و کیفیت برنامه افزایش یافت.
JCF دارای یک سلسله مراتب تعریف شده از رابط ها است. java.util.collection
سوپرواسط Iterable را گسترش می دهد. در داخل مجموعه، رابطها و کلاسهای نسل زیادی وجود دارد که در زیر نشان داده شده است:

نحوه سرعت بخشیدن به پردازش مجموعه های بزرگ در جاوا
همانطور که قبلا ذکر شد، مجموعه ها گروه های نامرتب از اشیاء منحصر به فرد هستند. از سوی دیگر، لیست ها مجموعه های مرتب شده ای هستند که ممکن است حاوی موارد تکراری باشند. در حالی که میتوانید عناصر را در هر نقطه از فهرست اضافه کنید، باقیمانده ترتیب حفظ میشود.
صف ها مجموعه هایی هستند که در آن عناصر در یک انتها اضافه می شوند و از انتهای دیگر حذف می شوند، به عنوان مثال یک رابط، اول ورود، اول خروج (FIFO) است. Deques (صف دو طرفه) امکان افزودن یا حذف عناصر از هر دو طرف را فراهم می کند.
• size (): returns the number of elements in a collection • add (Collection element) / remove (Collection object): as suggested, these methods alter the contents of a collection; note that in the event a collection has duplicates, remove only affects a single instance of the element. • equals (Collection object): compares an object for equivalence with a collection • clear (): removes every element from a collection
هر زیرمجموعه ممکن است متدهای اضافی نیز داشته باشد. به عنوان مثال، اگرچه رابط Set فقط شامل متدهای واسط مجموعه است، رابط List دارای روش های اضافی بسیاری بر اساس دسترسی به عناصر لیست خاص است، از جمله:
• get (int index): returns the list element from the specified index location • set (int index, element): sets the contents of the list element at the specified index location • remove (int,index): removes the element at the specified index location
عملکرد مجموعه های جاوا
همانطور که اندازه مجموعه ها بزرگ می شود، به همان نسبت می توانند مشکلات عملکرد قابل توجهی را ایجاد کنند. و به نظر می رسد که انتخاب مناسب انواع مجموعه و طراحی مجموعه مرتبط نیز می تواند به طور قابل ملاحظه ای بر عملکرد تأثیر بگذارد.
حجم روزافزون داده های موجود برای توسعه دهندگان و برنامه های کاربردی، جاوا را به معرفی راه های جدیدی برای پردازش مجموعه ها برای افزایش عملکرد کلی سوق داد. در جاوا 8 که در سال 2014 منتشر شد، جاوا Streams را معرفی کرد – قابلیت جدیدی که هدف آن ساده سازی و افزایش سرعت پردازش انبوه اشیاء بود. از زمان معرفی، استریم ها پیشرفت های زیادی داشته است.
درک این نکته ضروری است که جریان ها خود ساختار داده نیستند. در عوض، همانطور که جاوا توضیح میدهد، جریانها «کلاسهایی هستند که از عملیاتهای سبک عملکردی بر روی جریانهای عناصر، مانند تبدیلهای کاهشیافته در نقشهها در مجموعهها، پشتیبانی میکنند».
جریان ها از پایپ لاین روش ها برای پردازش داده های دریافتی از یک منبع داده مانند مجموعه استفاده می کنند.هر روش جریان ،یا یک روش میانی است (روش هایی که جریان های جدیدی را که می توانند پردازش بیشتری داشته باشند برمی گرداند) یا یک روش پایانی (پس از آن هیچ پردازش جریان اضافی امکان پذیر نیست). روش های میانی در پایپ لاین تنبل هستند. یعنی فقط در مواقع ضروری ارزیابی می شوند.
هر دو گزینه اجرای موازی و متوالی برای استریم ها وجود دارد. جریان ها به طور پیش فرض متوالی هستند.
استفاده از پردازش موازی برای بهبود عملکرد
پردازش مجموعه های بزرگ در جاوا می تواند دست و پا گیر باشد. در حالی که Streams کار با مجموعههای بزرگ و عملیات کدگذاری را در مجموعههای بزرگ ساده کرد، اما همیشه تضمینی برای بهبود عملکرد نبود. در واقع، برنامه نویسان اغلب دریافتند که استفاده از Streams در واقع سرعت پردازش را کند می کند.
همانطور که در مورد وبسایتها مشخص است، کاربران فقط چند ثانیه اجازه بارگذاری را میدهند تا قبل از اینکه ناامید از ادامه کار شوند. بنابراین برای ارائه بهترین تجربه ممکن برای مشتری و حفظ شهرت توسعهدهنده برای ارائه محصولات با کیفیت، توسعهدهندگان باید نحوه بهینهسازی تلاشهای پردازشی برای مجموعههای بزرگ داده را در نظر بگیرند. و در حالی که پردازش موازی نمیتواند سرعت بهبود یافته را تضمین کند، اما مکان امیدوارکنندهای برای شروع است.
پردازش موازی، یعنی شکستن وظیفه پردازش به قطعات کوچکتر و اجرای همزمان آنها، یکی از راههای کاهش هزینه پردازش در هنگام برخورد با مجموعههای بزرگ است. اما حتی پردازش جریان موازی نیز میتواند منجر به کاهش عملکرد شود، حتی اگر کدنویسی سادهتر باشد. در اصل، هزینههای سربار مرتبط با مدیریت موضوعات متعدد میتواند مزایای اجرای موازی موضوعات را جبران کند.
از آنجایی که مجموعه ها از نظر رشته ای ایمن نیستند، پردازش موازی می تواند منجر به تداخل رشته یا خطاهای ناسازگاری حافظه شود (زمانی که رشته های موازی تغییرات ایجاد شده در رشته های دیگر را نمی بینند، پس دیدگاه های متفاوتی از داده های مشابه دارند). چارچوب مجموعهها تلاش میکند از ناهماهنگی رشتهها در طول پردازش موازی با استفاده از بسته ها همگامسازی جلوگیری کند. در حالی که بسته می تواند مجموعه ای را از نظر نخ ایمن کند و امکان پردازش موازی کارآمدتر را فراهم کند، اما می تواند اثرات نامطلوبی داشته باشد. به طور خاص، همگامسازی میتواند باعث کشمکش رشتهها شود که میتواند منجر به کندی اجرای نخها یا توقف اجرای آن شود.
جاوا یک تابع پردازش موازی بومی برای مجموعه ها دارد: Collection.parallelstream.
یکی از تفاوتهای مهم بین پردازش جریان متوالی پیشفرض و پردازش موازی این است که ترتیب اجرا و خروجی که همیشه هنگام پردازش متوالی یکسان است، میتواند در هنگام استفاده از پردازش موازی از اجرا به اجرا متفاوت باشد.
در نتیجه، پردازش موازی به ویژه در شرایطی که ترتیب پردازش بر خروجی نهایی تأثیر نمی گذارد، مؤثر است. با این حال، در شرایطی که وضعیت یک رشته میتواند بر وضعیت رشته دیگر تأثیر بگذارد، پردازش موازی میتواند مشکلاتی ایجاد کند.
یک مثال ساده را در نظر بگیرید که در آن فهرستی از حساب های دریافتنی را برای لیستی از 1000 مشتری ایجاد می کنیم. ما می خواهیم تعیین کنیم که چه تعداد از این مشتریان مطالبات بیش از 25000 دلار دارند. ما می توانیم این بررسی را به صورت متوالی یا موازی با سرعت های مختلف پردازش انجام دهیم.
برای تنظیم مثال برای پردازش موازی، استفاده خواهیم کرد از:
import java.util.Random; import java.util.ArrayList; import java.util.List; class Customer { static int customernumber; static int receivables; Customer(int customernumber, int receivables) { this.customernumber = customernumber; this.receivables = receivables; } public int getCustomernumber() { return customernumber; } public void setCustomernumber(int customernumber) { this.customernumber = customernumber; } public int getReceivables() { return receivables; } public void setReceivables() { this.receivables = receivables; } } public class ParallelStreamTest { public static void main( String args[] ) { Random receivable = new Random(); int upperbound = 1000000; List < Customer > custlist = new ArrayList < Customer > (); for (int i = 0; i < upperbound; i++) { int custnumber = i + 1; int custreceivable = receivable.nextInt(upperbound); custlist.add(new Customer(custnumber, custreceivable)); } long t1 = System.currentTimeMillis(); System.out.println("Sequential Stream count: " + custlist.stream().filter(c -> c.getReceivables() > 25000).count()); long t2 = System.currentTimeMillis(); System.out.println("Sequential Stream Time taken:" + (t2 - t1)); t1 = System.currentTimeMillis(); System.out.println("Parallel Stream count: " + custlist.parallelStream().filter(c -> c.getReceivables() > 25000).count()); t2 = System.currentTimeMillis(); System.out.println("Parallel Stream Time taken:" + (t2 - t1)); } }
اجرای کد نشان می دهد که پردازش موازی ممکن است منجر به بهبود عملکرد هنگام پردازش مجموعه داده ها شود:
البته توجه داشته باشید که هر بار که کد را اجرا می کنید، نتایج متفاوتی به دست خواهید آورد. در برخی موارد، پردازش متوالی همچنان از پردازش موازی بهتر است.
در این مثال، ما از فرآیندهای بومی جاوا برای تقسیم داده ها و اختصاص رشته ها استفاده کردیم.
متأسفانه، تلاشهای پردازش موازی بومی جاوا همیشه در هر شرایطی سریعتر از پردازش متوالی نیست، و در واقع، اغلب کندتر هستند.
به عنوان یک مثال، پردازش موازی هنگام برخورد با لیست های پیوندی مفید نیست. در حالی که منابع داده مانند ArrayLists برای پردازش موازی به سادگی تقسیم می شوند، این موضوع در مورد LinkedLists صادق نیست. TreeMaps و HashSets جایی در این بین قرار دارند.
یک روش برای تصمیم گیری در مورد استفاده از پردازش موازی، مدل NQ اوراکل است. در مدل NQ، N نشان دهنده تعداد عناصر داده ای است که باید پردازش شوند. Q، به نوبه خود، مقدار محاسبات مورد نیاز برای هر عنصر داده است. در مدل NQ، شما حاصل ضرب N و Q را محاسبه میکنید، که اعداد بالاتر نشاندهنده احتمالات بالاتری است که پردازش موازی منجر به بهبود عملکرد میشود.
هنگام استفاده از مدل NQ، یک رابطه معکوس بین N و Q وجود دارد. یعنی هر چه مقدار محاسبات مورد نیاز برای هر عنصر بیشتر باشد، مجموعه دادهها برای پردازش موازی میتواند کوچکتر باشد تا مزایایی داشته باشد. یک قانون سرانگشتی این است که برای نیازهای محاسباتی کم، حداقل مجموعه داده 10000 مبنای استفاده از پردازش موازی است.
اگرچه خارج از حوصله این مقاله است، اما روش های پیشرفته تری برای بهینه سازی پردازش موازی در مجموعه های جاوا وجود دارد. به عنوان مثال، توسعه دهندگان پیشرفته می توانند پارتیشن بندی عناصر داده را در مجموعه تنظیم کنند تا عملکرد پردازش موازی را به حداکثر برسانند. همچنین افزونه ها و جایگزین های شخص ثالثی برای JCF وجود دارد که می تواند عملکرد را بهبود بخشد. اما مبتدیان و توسعه دهندگان متوسط، باید بر درک این نکته تمرکز کنند که کدام عملیات از ویژگی های پردازش موازی بومی جاوا برای جمع آوری داده ها سود می برد.
نتیجه
در دنیای کلان داده، یافتن راههایی برای بهبود پردازش مجموعههای دادههای بزرگ برای ایجاد صفحات وب و برنامههای کاربردی با کارایی بالا ضروری است. جاوا ویژگیهای پردازش مجموعه داخلی را ارائه میکند که به توسعهدهندگان کمک میکند پردازش دادهها را بهبود بخشند، از جمله چارچوب مجموعهها و توابع پردازش موازی بومی. توسعهدهندگان باید با نحوه استفاده از این ویژگیها آشنا شوند و بفهمند که چه زمانی ویژگیهای بومی قابل قبول هستند و چه زمانی باید به پردازش موازی تغییر کنند.