تکامل مجموعهها در جاوا 9 و جاوا 10
به عنوان یک توسعهدهنده خیلی راحت ممکن است که به ابزارها و یا روتینهای خاصی عادت کنیم. مخصوصا اگر زبان برنامهنویسیای که استفاده میکنیم، مانند جاوا در دورههای طولانیمدت پایداری نسبتا خوبی داشته باشد. اما باید بدانیم که جاوا دیگر به این صورت نیست و هر 6 ماه یکبار نسخهی جدیدی منتشر میکند و بنابراین، میتوانیم انتظار داشته باشیم که مکررا شاهد بهروزرسانیهایی در آن باشیم.
این بهروزرسانیها ممکن است شامل تغییرات ساختاری وسیع مانند بهروزرسانیهای API یا Java Module System باشند. از آنجایی که هر نسخهی جدید ممکن است ویژگی مفیدی برای ما داشته باشد، با بهروز ماندن میتوانیم به توسعهدهندگان بهتر و موثرتری تبدیل شویم. در این مقاله، به طور خاص به بهروزرسانیهایی که در مجموعهها(Collections) در جاوا 9 و جاوا 10 انجام شده است، نگاهی میاندازیم. مجموعههای جاوا یک بخش اصلی از زبان هستند و ما احتمالا هر روز به طریقی با آنها روبرو شدهایم. پس هر چیزی که کار با آنها را راحتتر کند، کار ما را هم در کل راحتتر کرده است.
جاوا 9: متدهای Factory سادهتر برای مجموعهها
جاوا 9 روش جدیدی برای ایجاد مجموعههای تغییرناپذیر ارایه کرده است. همگی ما کدی شبیه به این را نوشتهایم:
List<String> moods = Arrays.asList("HAPPY", "SAD");
List<String> moods = List.of("HAPPY", "SAD");
jshell> List<String> moods = Arrays.asList("HAPPY", "SAD"); moods ==> [HAPPY, SAD] jshell> moods.add("ANGRY") | java.lang.UnsupportedOperationException thrown | at AbstractList.add (AbstractList.java:153) | at AbstractList.add (AbstractList.java:111) | at (#2:1)
jshell> moods.set(0, "ANGRY") $3 ==> "HAPPY" jshell> System.out.println(moods) [ANGRY, SAD]
احتمالا زمانی که این کد را نوشتیم، چیزی مانند این را میخواستیم:
List<String> moods = Collections.unmodifiableList(Arrays asList("HAPPY", "SAD"));
List<String> moods = List.of("HAPPY", "SAD");
در عبارت بالا یک لیست تغییرناپذیر برگردانده میشود و نه تنها کوتاهتر از معادلش تا قبل از جاوا 9 است، بلکه در مقایسه با استفاده از Arrays.asList، درستتر هم هست.
اتفاق بهتر این است که متدهای کاملا مشابهی برای Setها و Mapها نیز در اختیار ما قرار گرفته است. برای ایجاد یک Set تغییرناپذیر تا قبل از جاوا 9، باید کاری مشابه زیر را انجام میدادیم:
Set<String> moods = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("HAPPY", "SAD")));
که واقعا ناخوشایند است. اما در جاوا 9، این کار به سادگیِ زیر قابل انجام است:
Set<String> moods = Set.of("HAPPY", "SAD");
همچنین متدهای factory برای ایجاد Mapها نیز وجود دارد. تا قبل از جاوا 9، اگر میخواستیم یک Map با استفاده از یک مجموعهی ثابت مقادیر بسازیم، باید این کار را کمی طولانیتر انجام میدادیم:
Map<String, Mood> wordToMood = new HashMap<>(); wordToMood.put("happy", HAPPY); wordToMood.put("good", HAPPY); wordToMood.put("great", HAPPY); //… more values wordToMood.put("horrible", SAD); wordToMood.put("bad", SAD); wordToMood.put("awful", SAD);
اگر میخواستیم Map را به صورت یک مقدار ثابت (consant) مقداردهی کنیم، اوضاع از این هم سختتر میشد. زیرا مجبور بودیم آن را جایی در بلاک static قرار دهیم و با یک unmodifiableMap آن را wrap کنیم که واضحا پیچیدگی را بیشتر و بیشتر میکرد. اما حالا با جاوا 9، میتوانیم به این صورت عمل کنیم:
Map<String, Mood> wordToMood = Map.ofEntries(Map.entry("happy", HAPPY), Map.entry("good", HAPPY), Map.entry("great", HAPPY) //...more values Map.entry("horrible", SAD), Map.entry("bad", SAD), Map.entry("awful", SAD));
البته با importهای static میتوان آن را مختصرتر هم بیان کرد. متد Map.ofEntries با هر تعداد دلخواهی از زوجهای کلید/مقدار کار میکند. به این صورت که در یک Map.entry هر زوج wrap شده است و متد ofEntries یک پارامتر vararg دریافت میکند. اگر Map کمتر از 10 مقدار داشته باشد، ممکن است بخواهیم از متد سادهی Map.of استفاده کنیم که تا 10 زوج کلید/مقدار را به عنوان ورودی میتواند دریافت کند.
Map<String, Mood> wordToMood = Map.of("happy", HAPPY, "good", HAPPY, "great", HAPPY, "horrible", SAD, "bad", SAD, "awful", SAD);
جاوا 10: ایجاد کپیهای تغییرناپذیر از مجموعهها
جاوا 10 ایجاد مجموعههای تغییرناپذیر را با استفاده از عملیاتهای جویبار (Stream) و با اضافه کردن متدهای toUnmodifiableList ،toUnmodifiableSet و toUnmodifiableMap ساده میکند. به این معنی که پس از جاوا 10، شما نه تنها با یکسری مقادیر مشخصشده و یا با کپی کردن مجموعهها یا Mapهای موجود، بلکه با استفاده از جویبارها هم میتوانید مجموعههای تغییرناپذیر بسازید.
برای مثال، بیایید فرض کنیم که یک عملیات جویبار داریم که یک جمله را میگیرد و لیستی از کلمات یکتا را بر میگرداند:
List<String> uniqueWords = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*").splitAsStream(message) .map(String::toLowerCase) .distinct() .collect(Collectors.toList());
این یک ArrayList ساده است و میتواند به صورت زیر تغییر کند:
jshell> String message = "I am so so happy today, and I am not happy every day"; message ==> "I am so so happy today, and I am not happy every day" jshell> List<String> uniqueWords = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*").splitAsStream(message). ...> map(String::toLowerCase). ...> distinct(). ...> collect(Collectors.toList()); uniqueWords ==> [i, am, so, happy, today, and, not, every, day] jshell> uniqueWords.getClass() $35 ==> class java.util.ArrayList jshell> uniqueWords.add("SAD") $36 ==> true jshell> System.out.println(uniqueWords) [i, am, so, happy, today, and, not, every, day, SAD]
اگر هدف این عملیات جویبار، بازگرداندن یک مجموعه نتایج ثابت بود، احتمالا نمیخواستیم یک ArrayList برگردانیم و نوعی لیست تغییرناپذیر میخواستیم. با جاوا 10، میتوانیم این کار را انجام دهیم:
List<String> uniqueWords = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*" .splitAsStream(message) .map(String::toLowerCase) .distinct() .collect(Collectors.toUnmodifiableList());
با این کار، یک لیست تغییرناپذیر برگردانده میشود:
jshell> List<String> uniqueWords = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*").splitAsStream(message). ...> map(String::toLowerCase). ...> distinct(). ...> collect(Collectors.toUnmodifiableList()); uniqueWords ==> [i, am, so, happy, today, and, not, every, day] jshell> uniqueWords.getClass() $39 ==> class java.util.ImmutableCollections$ListN jshell> uniqueWords.add("SAD") | java.lang.UnsupportedOperationException thrown | at ImmutableCollections.uoe (ImmutableCollections.java:71) | at ImmutableCollections$AbstractImmutableList.add (ImmutableCollections.java:77) | at (#40:1)
همچنین Collectors.toUnmodifiableSet هم وجود دارد که برای این سناریو که مجموعه تنها شامل مقادیر یکتاست، میتواند مناسبتر باشد.
Collectors.toUnmodifiableMap برای ایجاد Mapهای تغییرناپذیر است و مانند toMap، کمی پیچیدهتر است زیرا به این معنی است که نیاز داریم توابعی تعریف کنیم که بگوید کلید و مقدارها چه چیزهایی هستند. اگر مثالمان را تغییر دهیم تا با استفاده از یک Map تعداد تکرار هر کلمه در جمله را محاسبه کند، میتوانیم نشان دهیم که چگونه میتوان نتایج را در یک Map تغییرناپذیر داشت:
Map<String, Long> wordCount = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*"). splitAsStream(message). map(String::toLowerCase). collect(Collectors.toUnmodifiableMap(Function.identity(), word -> 1L, (oldCount, newVal) -> oldCount + newVal));
مانند قبل، میتوانیم ببینیم که مقادیر نمیتوانند به این Map اضافه شوند و یا از آن حذف شوند:
jshell> Map<String, Long> wordCount = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*"). splitAsStream(message). ...> map(String::toLowerCase). ...> collect(Collectors.toUnmodifiableMap(Function.identity(), ...> word -> 1L, ...> (oldCount, newVal) -> oldCount + newVal)); wordCount ==> {and=1, i=2, am=2, day=1, so=2, every=1, today=1, not=1, happy=2} jshell> wordCount.getClass() $49 ==> class java.util.ImmutableCollections$MapN jshell> wordCount.put("WORD", 1000L) | java.lang.UnsupportedOperationException thrown | at ImmutableCollections.uoe (ImmutableCollections.java:71) | at ImmutableCollections$AbstractImmutableMap.put (ImmutableCollections.java:558) | at (#50:1)
در نتیجه، میتوانیم ببینیم که جاوا در حال تکامل است تا کار را برای ما برنامهنویسان جهت نوشتن کد درست و مناسب آسانتر کند. برخی از این تغییرات، تنها چیزهای کوچکی هستند که به API موجود اضافه شدهاند و به سادگی ممکن است در کنار تغییرات بزرگترِ زبان، نادیده گرفته شوند. مجموعهها در نسخههای اخیر تکامل یافتهاند و اگر با این تغییرات بهروز بمانیم، زندگیمان کمی آسانتر خواهد شد.