"جاوا هنوز زنده است - و مردم شروع به درک آن کرده اند."
به معرفی من برای جاوا 8 خوش آمدید. این راهنما شما را گام به گام با تمام ویژگی های جدید این زبان آشنا می کند. از طریق نمونههای کد کوتاه و ساده، نحوه استفاده از روشهای پیشفرض رابط ، عبارات لامبدا ، روشهای مرجع و حاشیهنویسیهای تکرارپذیر را خواهید آموخت . تا پایان مقاله، با آخرین تغییرات APIها مانند استریم ها، رابط های تابع، پسوندهای انجمن و API جدید Date آشنا خواهید شد. بدون دیوار متن خسته کننده - فقط یک دسته از تکه های کد نظر داده شده. لذت ببرید!روش های پیش فرض برای رابط ها
جاوا 8 به ما اجازه می دهد تا روش های غیرانتزاعی پیاده سازی شده در رابط را با استفاده از کلمه کلیدی پیش فرض اضافه کنیم . این ویژگی به عنوان روش های توسعه نیز شناخته می شود . این اولین مثال ما است:interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } }
علاوه بر
محاسبه متد انتزاعی ، رابط
فرمول یک روش پیشفرض
sqrt را نیز تعریف میکند . کلاس هایی که رابط
فرمول را پیاده سازی می کنند فقط روش
محاسبه انتزاعی را اجرا می کنند . روش پیش فرض
sqrt را می توان مستقیماً از جعبه استفاده کرد.
Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0
شی
فرمول به عنوان یک شی ناشناس پیاده سازی می شود. کد بسیار چشمگیر است: 6 خط کد برای محاسبه
sqrt (a * 100) . همانطور که در بخش بعدی خواهیم دید، روش جذاب تری برای پیاده سازی اشیاء تک متد در جاوا 8 وجود دارد.
عبارات لامبدا
بیایید با یک مثال ساده از نحوه مرتبسازی آرایهای از رشتهها در نسخههای اولیه جاوا شروع کنیم: روش کمکی آماری Collections.sort یک لیست و یک مقایسه کننده برای مرتب کردن عناصر لیست داده شده میگیرد. آنچه اغلب اتفاق می افتد این است که مقایسه کننده های ناشناس ایجاد می کنید و آنها را برای مرتب سازی روش ها ارسال می کنید. جاوا 8 به جای ایجاد دائمی اشیاء ناشناس، این توانایی را به شما می دهد که از نحو بسیار کمتری استفاده کنید، عبارات لامبدا : همانطور که می بینید، کد بسیار کوتاه تر و خواندن آسان تر است. اما در اینجا حتی کوتاهتر میشود: برای روش تکخطی، میتوانید از دستبندهای فرفری {} و کلمه کلیدی بازگشتی خلاص شوید . اما اینجاست که کد حتی کوتاهتر میشود: کامپایلر جاوا از انواع پارامترها آگاه است، بنابراین میتوانید آنها را نیز کنار بگذارید. حال بیایید عمیق تر به نحوه استفاده از عبارات لامبدا در زندگی واقعی بپردازیم.List
names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator
() { @Override public int compare(String a, String b) { return b.compareTo(a); } });
Collections.sort(names, (String a, String b) -> { return b.compareTo(a); });
Collections.sort(names, (String a, String b) -> b.compareTo(a));
Collections.sort(names, (a, b) -> b.compareTo(a));
رابط های کاربردی
چگونه عبارات لامبدا در سیستم نوع جاوا قرار می گیرند؟ هر لامبدا مربوط به نوع معینی است که توسط یک رابط تعریف شده است. و به اصطلاح رابط کاربردی باید دقیقاً حاوی یک روش انتزاعی اعلام شده باشد. هر عبارت لامبدا از یک نوع معین با این متد انتزاعی مطابقت دارد.از آنجایی که متدهای پیشفرض متدهای انتزاعی نیستند، میتوانید متدهای پیشفرض را به رابط کاربری خود اضافه کنید. ما می توانیم از یک رابط دلخواه به عنوان عبارت لامبدا استفاده کنیم، مشروط بر اینکه رابط فقط یک روش انتزاعی داشته باشد. برای اطمینان از اینکه رابط شما این شرایط را برآورده می کند، باید حاشیه نویسی @FunctionalInterface را اضافه کنید . کامپایلر با این حاشیه مطلع می شود که اینترفیس باید فقط یک متد داشته باشد و اگر در این رابط با متد انتزاعی دوم مواجه شود، خطا می دهد. مثال: به خاطر داشته باشید که حتی اگر حاشیه نویسی @FunctionalInterface اعلام نشده باشد، این کد نیز معتبر خواهد بود.@FunctionalInterface interface Converter
{ T convert(F from); }
Converter
converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123
ارجاع به روش ها و سازنده ها
مثال بالا را می توان با استفاده از یک مرجع روش آماری ساده تر کرد: جاوا 8 به شما امکان می دهد ارجاعات را به متدها و سازنده ها با استفاده از نمادهای کلمه کلیدی :: منتقل کنید . مثال بالا نشان می دهد که چگونه می توان از روش های آماری استفاده کرد. اما ما همچنین میتوانیم به روشها روی اشیاء ارجاع دهیم: بیایید نگاهی به نحوه کارکرد :: برای سازندهها بیندازیم. ابتدا مثالی را با سازنده های مختلف تعریف می کنیم: در مرحله بعد، رابط کارخانه PersonFactory را برای ایجاد اشیاء شخص جدید تعریف می کنیم :Converter
converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123
class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } }
Something something = new Something(); Converter
converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J"
class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
interface PersonFactory
{ P create(String firstName, String lastName); }
به جای پیاده سازی کارخانه به صورت دستی، همه چیز را با استفاده از یک مرجع سازنده به هم گره می زنیم: از طریق Person::new یک مرجع به سازنده کلاس Person ایجاد می کنیم . کامپایلر جاوا به طور خودکار سازنده مناسب را با مقایسه امضای سازنده ها با امضای متد PersonFactory.create فراخوانی می کند . PersonFactory
منطقه لامبدا
سازماندهی دسترسی به متغیرهای محدوده بیرونی از عبارات لامبدا مشابه دسترسی از یک شی ناشناس است. می توانید به متغیرهای نهایی از محدوده محلی و همچنین فیلدهای نمونه و متغیرهای انبوه دسترسی داشته باشید.دسترسی به متغیرهای محلی
ما میتوانیم یک متغیر محلی را با اصلاحکننده نهایی از محدوده یک عبارت لامبدا بخوانیم : اما برخلاف اشیاء ناشناس، متغیرها نیازی به اعلام نهایی ندارند تا از یک عبارت لامبدا قابل دسترسی باشند . این کد نیز صحیح است: با این حال، متغیر num باید تغییرناپذیر باقی بماند، i.e. نهایی ضمنی برای کامپایل کد باشد . کد زیر کامپایل نمی شود: تغییر در num در یک عبارت لامبدا نیز مجاز نیست.final int num = 1; Converter
stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3
int num = 1; Converter
stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3
int num = 1; Converter
stringConverter = (from) -> String.valueOf(from + num); num = 3;
دسترسی به فیلدهای نمونه و متغیرهای آماری
برخلاف متغیرهای محلی، میتوانیم فیلدهای نمونه و متغیرهای آماری را در عبارات لامبدا بخوانیم و تغییر دهیم. ما این رفتار را از اشیاء ناشناس می دانیم.class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter
stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter
stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
دسترسی به روش های پیش فرض رابط ها
مثال با فرمول نمونه از بخش اول را به خاطر دارید؟ رابط فرمول یک روش پیشفرض sqrt را تعریف میکند که میتوان از هر نمونه از فرمول ، از جمله اشیاء ناشناس، به آن دسترسی داشت. این با عبارات لامبدا کار نمی کند. در داخل عبارت لامبدا نمی توان به روش های پیش فرض دسترسی داشت. کد زیر کامپایل نمی شود:Formula formula = (a) -> sqrt( a * 100);
رابط های کاربردی داخلی
JDK 1.8 API شامل بسیاری از رابط های کاربردی داخلی است. برخی از آنها از نسخه های قبلی جاوا به خوبی شناخته شده اند. برای مثال Comparator یا Runnable . این رابطها برای شامل پشتیبانی لامبدا با استفاده از حاشیهنویسی FunctionalInterface@ گسترش یافتهاند . اما Java 8 API همچنین پر از رابط های کاربردی جدید است که زندگی شما را آسان تر می کند. برخی از این رابطها از کتابخانه Guava گوگل به خوبی شناخته شدهاند . حتی اگر با این کتابخانه آشنایی دارید، باید نگاهی دقیقتر به نحوه گسترش این رابطها، با چند روش توسعه مفید بیندازید.محمولات
محمول ها توابع بولی با یک آرگومان هستند. رابط شامل روشهای پیشفرض مختلفی برای ایجاد عبارات منطقی پیچیده (و، یا، نفی) با استفاده از گزارهها استPredicate
predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate
nonNull = Objects::nonNull; Predicate
isNull = Objects::isNull; Predicate
isEmpty = String::isEmpty; Predicate
isNotEmpty = isEmpty.negate();
کارکرد
توابع یک آرگومان می گیرند و یک نتیجه تولید می کنند. روش های پیش فرض را می توان برای ترکیب چندین تابع با هم در یک زنجیره (نوشتن و سپس) استفاده کرد.Function
toInteger = Integer::valueOf; Function
backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
تامین کنندگان
تامین کنندگان یک نتیجه (نمونه) از یک نوع یا دیگری را برمی گردانند. بر خلاف توابع، ارائه دهندگان آرگومان ها را نمی گیرند.Supplier
personSupplier = Person::new; personSupplier.get(); // new Person
مصرف کنندگان
مصرف کنندگان روش های رابط را با یک آرگومان واحد نمایش می دهند.Consumer
greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
مقایسه کننده ها
مقایسه کننده ها از نسخه های قبلی جاوا برای ما شناخته شده اند. جاوا 8 به شما اجازه می دهد تا روش های پیش فرض مختلفی را به رابط ها اضافه کنید.Comparator
comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
اختیاری
رابط Optionals کاربردی نیست، اما یک ابزار عالی برای جلوگیری از NullPointerException است . این یک نکته مهم برای بخش بعدی است، بنابراین بیایید نگاهی گذرا به نحوه عملکرد این رابط بیندازیم. رابط اختیاری یک محفظه ساده برای مقادیری است که می توانند null یا non null باشند. تصور کنید که یک متد می تواند یک مقدار یا چیزی را برگرداند. در جاوا 8، به جای برگرداندن null ، یک نمونه اختیاری را برمی گردانید .Comparator
comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
جریان
java.util.Stream دنباله ای از عناصر است که یک یا چند عملیات روی آن انجام می شود. هر عملیات Stream یا میانی یا پایانی است. عملیات ترمینال یک نتیجه از یک نوع خاص را برمیگرداند، در حالی که عملیات میانی خود شی جریان را برمیگرداند و امکان ایجاد زنجیرهای از فراخوانیهای متد را فراهم میکند. Stream یک رابط است، مانند java.util.Collection برای لیست ها و مجموعه ها (نقشه ها پشتیبانی نمی شوند). هر عملیات Stream می تواند به صورت متوالی یا موازی اجرا شود. بیایید نگاهی به نحوه عملکرد استریم بیندازیم. ابتدا، نمونه کد را به شکل لیستی از رشتهها ایجاد میکنیم: مجموعهها در جاوا 8 افزایش یافتهاند تا بتوانید به سادگی با فراخوانی Collection.stream یا Collection.parallelStream () استریم ایجاد کنید . بخش بعدی مهمترین و ساده ترین عملیات جریان را توضیح می دهد.List
stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
فیلتر کنید
فیلتر برای فیلتر کردن همه عناصر جریان، گزارههایی را میپذیرد. این عملیات میانی است، که به ما اجازه میدهد تا عملیات جریانی دیگر (مثلاً forEach) را بر روی نتیجه (فیلتر شده) فراخوانی کنیم. ForEach عملیاتی را می پذیرد که بر روی هر عنصر از جریان فیلتر شده قبلی انجام می شود. ForEach یک عملیات ترمینال است. علاوه بر این، فراخوانی سایر عملیات غیرممکن است.stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
مرتب شده است
Sorted یک عملیات میانی است که یک نمایش مرتب شده از جریان را برمی گرداند. عناصر به ترتیب صحیح مرتب می شوند مگر اینکه مقایسه کننده خود را مشخص کنید .stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2"
به خاطر داشته باشید که مرتب شده یک نمایش مرتب شده از جریان ایجاد می کند بدون اینکه بر خود مجموعه تأثیر بگذارد. ترتیب عناصر
stringCollection دست نخورده باقی می ماند:
System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
نقشه
عملیات نقشه میانی هر عنصر را با استفاده از تابع حاصل به شی دیگری تبدیل می کند. مثال زیر هر رشته را به یک رشته بزرگ تبدیل می کند. اما شما همچنین می توانید از نقشه برای تبدیل هر شی به نوع دیگری استفاده کنید. نوع اشیاء جریان حاصل به نوع تابعی که برای نقشه ارسال می کنید بستگی دارد.stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
همخوانی داشتن
عملیات تطبیق مختلفی را می توان برای آزمایش درستی یک محمول خاص در رابطه جریان استفاده کرد. همه عملیات تطبیق پایانی هستند و یک نتیجه بولی برمیگردانند.boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
شمردن
شمارش یک عملیات پایانی است که تعداد عناصر جریان را به صورت طولانی برمی گرداند .long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
كاهش دادن
این یک عملیات ترمینال است که عناصر جریان را با استفاده از تابع عبور کوتاه می کند. نتیجه یک گزینه اختیاری خواهد بود که حاوی مقدار کوتاه شده است.Optional
reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
جریان های موازی
همانطور که در بالا ذکر شد، جریان ها می توانند متوالی یا موازی باشند. عملیات جریان متوالی بر روی یک رشته سریال انجام می شود، در حالی که عملیات جریان موازی بر روی چندین رشته موازی انجام می شود. مثال زیر نشان می دهد که چگونه می توان به راحتی عملکرد را با استفاده از یک جریان موازی افزایش داد. ابتدا، بیایید یک لیست بزرگ از عناصر منحصر به فرد ایجاد کنیم: اکنون زمان صرف شده برای مرتب سازی جریان این مجموعه را تعیین می کنیم.int max = 1000000; List
values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
جریان سریال
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
جریان موازی
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms
همانطور که می بینید، هر دو قطعه تقریباً یکسان هستند، اما مرتب سازی موازی 50٪ سریعتر است. تنها چیزی که نیاز دارید این است که
stream() را به
parallelStream() تغییر دهید .
نقشه
همانطور که قبلا ذکر شد، نقشه ها از جریان ها پشتیبانی نمی کنند. در عوض، نقشه شروع به پشتیبانی از روشهای جدید و مفید برای حل مشکلات رایج کرد. کد بالا باید بصری باشد: putIfAbsent به ما درباره نوشتن چک های تهی اضافی هشدار می دهد. forEach یک تابع را برای اجرای هر یک از مقادیر نقشه می پذیرد. این مثال نحوه انجام عملیات بر روی مقادیر نقشه را با استفاده از توابع نشان می دهد: در مرحله بعد، ما یاد خواهیم گرفت که چگونه یک ورودی را برای یک کلید داده شده حذف کنیم فقط در صورتی که به یک مقدار مشخص نگاشت شود: روش خوب دیگر: ادغام ورودی های نقشه بسیار آسان است: ادغام اگر ورودی برای کلید داده شده وجود نداشته باشد، یا کلید/مقدار را در نقشه وارد می کند، یا تابع ادغام فراخوانی می شود که مقدار ورودی موجود را تغییر می دهد.Map
map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val));
map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33
map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null
map.getOrDefault(42, "not found"); // not found
map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
GO TO FULL VERSION