جاوا در نسخه 8 خود تغییراتی داشته که از ابتدا تا کنون جز پر اهمیت ترین تغییرات میباشد و یادگیری آن بسیار حائز اهمیت است
این ویژگی ها در جاوا 8 تمرکزشان روی what to do است و نه مثل قبل how to do
یعنی تمرکز را بگذاریم روی نتیجه بجای اینکه تمرکز را روی نحوه پردازش گذاشته باشیم
default method in interface : یک اینترفیس مانند یک کلاس است که همه متد های آن Abstract هستند ولی از جاوا 8 به بعد یک اینترفیس میتواند یک متد دارای بدنه هم داشته باشد که به این متد ها default method گفته میشود و با کلمه کلیدی default مشخص میشوند
interface BankAccount{ long getBalance(); default long getBankBranchName(){ return "khojaste"; } }
متد های default میتواند در کلاس فرزند override شوند
نکته : همانطور که میبینم در جاوا 8 میتوانیم اینترفیس هایی با متد هایی از نوع default داشته باشیم که پیاده سازی شده اند و در کلاس ها میتوانیم بیش از یک اینترفیس را ارث بری کنیم و طبیعتا multiple inheritance خواهیم داشت ! اما ارث بری چندگانه آیا خطرناک است ؟
ابتدا باید بگوییم که ما چند نوع وراثت چندگانه داریم : وراثت از نوع ، حالت و رفتار
خطرناکترین نوع وراثت چندگانه حالت است که متغییر ها و ویژگی های چند کلاس کاملا به ارث برده میشود با وجود اینترفیس بودن کلاس ها وراثت چند گانه از نوع حالت همچنان نداریم
Functional Interface : اینترفیسی که فقط یک متد abstract داشته باشد functional interface گفته میشود و در بالای یک functional interface میتوانیم FunctionalInterface@ را برای خوانایی بیشتر بگذاریم
Lambda : یک تکه کدی است که بدنه یک تابع را تعریف میکند همانند ریاضی که داشتیم :
r -> r * 2 * 3.14 (x,y) -> x + y
هر عبارت لامبدا میتواند به عنوان یک Functional Interface استفاده شود چون هر functional interface فقط یک متد abstract دارد به عنوان مثال :
public interface Comprator<T>{ int compare(T o1, T o2); }
اینترفیس Comprator که در جاوا برای مرتب سازی و مقایسه دو ابجکت مورد استفاده قرار میگیرد تنها یک متد abstract دارد و میتواند توسط lambda بدنه آنرا تعریف کرد
Comprator <Person> comp = (a,b) -> a.age().compareTo(b.age());
و یا برای مرتب سازی قبلا از روش زیر استفاده میکردیم :
Collections.sort( list , new Comprator<Person>(){ @Override public int compare (Person a , Person b ){ return a.age().compareTo(b.age()); } });
را میتوانیم به این صورت با lambda جایگزین کنیم :
Collections.sort( list , (a,b) -> a.age().compareTo(b.age());
Method Reference : از :: برای ارجاع به یک متد میتوان استفاده کرد به عنوان مثال کلاسی داریم بنام Str که یک متدی بنام startsWith دارد که کاراکتر اول رشته ورودی را برمیگرداند
class Str{ Character startsWith(String s) { return s.charAt(0); } }
و یک Functional Interface داریم بنام Converter که یک متد abstract بنام convert دارد و کارش گرفتن یک ورودی و دادن یک خروجی است :
@FunctionalInterface interface Converter <F,T> { T convert (F f); }
حال میخواهیم بدنه پیاده سازی شده متد startsWith در کلاس Str را به متد abstract ارجاع دهیم :
Str str = new Str(); Converter <String , Character> conv = str::startsWith; Character char = conv.convert("Human");
ارجاع به Constractor :
@FunctionalInterface interface Factory<T> { T create(); } Factory<Car> factory = Car::new; Car car = factory.create();
هر جا که یک functional interface داشته باشیم میتوانیم از Method Reference و Lambda استفاده کنیم و بدنه آن متد را تعیین کنیم
بسیاری از interface های معروفی که قبلا استفاده میکردیم در جاوا 8 تبدیل به functional interface شده اند مانند Comprator و Runnable و ...
اینترفیس های جدیدی هم در جاوا 8 اضافه شده اند مانند Predicate , Function ,Supplier , Consumer که در پکیج java.until.function قرار دارند
اینترفیس تابعی predicate : مثل یک تابع است که یک ورودی توسط متد test میگیرد و یک مقدار boolean بر میگرداند و برای ترکیب predicate ها از and , or , negate میتوان استفاده کرد
String str = "input"; Predicate <String> isNotEmpty = (a) -> a.length() > 0 ; boolean bol = false; bol= isNotEmpty.test(str); //true bol= isNotEmpty.negate().test(str); //false Predicate<String> notNull = a -> a!=null; bol = notNull.and(isNotEmpty).test(str); //true Predicate<String> isEmpty = String::isEmpty; Predicate<String> isNotEmpty2 = isEmpty.negate();
Function<String,Integer> toInteger = Integer::valueOf; Function<String,String> backToString= toInteger.andThen(String::valueOf); backToString.apply("12345") ; // "12345"
اینترفیس تابعی Supplier : یک تابعی است که یک ابجکت با کمک متد get تولید میکند و هیچ پارامتر ورودی هم ندارد
Supplier<Person> personSupplier = Person::new; Person person = personSupplier.get(); // new person
اینترفیس تابعی Consumer : تابعی که یک پارامتر ورودی میگیرد و خروجی ندارد و با متد accept اجرا میشود
Consumer<Person> greeter = p -> System.out.println(p.getFirstName()); greeter.accept(person);
اینترفیس تابعی BiFunction دو پارامتر از جنس T و U میگیرد و یک خروجی از نوع V تولید میکند
BiFunction<Integer , Integer , String> biFunction= (num1, num2) -> "Result: " + (num1 + num2); System.out.println(biFunction.apply(20,25));
اینترفیس تابعی UnaryOperator یک پارامتر از جنس T میگیرد و یک خروجی از جنس T تولید میکند
اینترفیس تابعی BinaryOperator دو پارامتر از جنس T میگیرد و یک خروجی از جنس T تولید میکند
اینترفیس تابعی BiConsumer که دو پارامتر از جنس T و U میگیرد و خروجی ای تولید نمیکند
Map<Integer,String> map ; Biconsumer<Integer,String> biConsumer = (key,value) -> System.out.println("key: " + key + " value: "+value); map.forEach(biConsumer);
اینترفیس تابعی BiPredicate که دو پارامتر از جنس T و U میگیرد و خروجی از جنس boolean بر میگرداند نمیکند
BiPredicate<Integer,String> condition = (i , s) -> i.toString().equals(s); System.out.println(condition.test(10 , "10")); //true System.out.println(condition.test(10 , "20")); //false
کلاس Optional : از این کلاس برای کاهش خطای NullPointerException استفاده میشود . Optional یک کلاس Generic است که هر نوع ابجکتی را میتواند نگهداری کند
بعد از بوجود امدن این کلاس خروجی خیلی از کتابخانه های جدید هم بصورت Optional تعریف شده اند
چنانچه ابجکت درون آن null باشد میتوان قبل از اینکه به خطا NullPointerException بر بخوریم null بودن آنرا براحتی چک کنیم
متد isPresent اگر مقدار داخلش null نباشد true بر میگرداند
متد get ابجکت داخلی را بر میگرداند
متد orElse اگر ابجکت داخلی وجود داشت آنرا بر میگرداند وگرنه ابجکتی که داخل آرگومان ارسال کردیم را بر میگرداند
متد ifPresent یک آرگومان از جنس Consumer میگیرد و اگر ابجکت داخلی موجود بود آنرا به Consumer ارسال و اجرا میکند
Stream : یک دنباله ای از ابجکت ها است که ما میتوانیم روی این دنباله یک یا چند عملیات را انجام دهیم که معمولا از یک Collection ایجاد شده است ولی میتواند از روی منابعی دیگری مانند آرایه ، کانال I/O ، یا یک تابع مولد ایجاد شود
Stream میتواند روی اعضای دنباله ایجاد شده پیمایش کند و عملیاتی را روی آنها انجام دهد که این عملیات شامل دو نوع هستند :
- عملیات پایانی یا terminal که یک داده ای را برمیگرداند مثل (Integer , String) و یا بصورت void میتواند باشد
- عملیات میانی یا intermediate که همان stream را بر میگرداند و میتوان زنجیر وار عملیاتی دیگر را انجام داد
نکته : تا موقعی که یک عمل terminal یا پایانی فراخوانی نشود هیچکدام از عملیات میانی یا intermediate اجرا نمیشوند
بر روی هر Collection میتوانیم یک Stream ایجاد بکنیم
List<String> list ; Stream<String> stream = list.stream(); stream.forEach(System.out::println);
مثال : یک لیست ماشین داریم که میخواهیم دو ماشین با کمترین قیمت و رنگ مشکی را در خروجی چاپ کنیم :
List<Car> list = ...; list.stream() .filter( a -> "Black".equals(a.color)) .sorted((a,b) -> a.price - b.price) .limit(2) .forEach(System.out:println);
متد parallel : میتوانیم اجرای عملیات را بصورت MultiThread نیز انجام دهیم کافی است متد parallel قبل از اجرای عملیات بیاوریم
list.stream().parallel() .filter( a -> "Black".equals(a.color)) .sorted((a,b) -> a.price - b.price) .limit(2) .forEach(System.out:println);
اما باید حواسمان در استفاده کردن از این خاصیت باشد چون ممکن است باعث کاهش پرفرمانس و همچنین خروجی همراه با خطا باشد
مثال برای دیدن کاهش پرفرمانس :
Stream.iterate(1 , i -> i+1) .limit(n) .parallel() .reduce( (a,b) -> a + b);
در کد بالا اگر parallel را حذف کنیم چندین برابر سرعت کد بالاتر میرود چرا ؟ چون iterate کردن نتیجه یک عملیات متوالی است و باید عضو بعدی از روی عضو قبلی ساخته شود و تقسیم کردن چنین عملیاتی در چند ترد این عملیات را کندتر میکند پس متد iterate برای parallel مناسب نیست و تجزیه پذیری ضعیفی دارد
تمامی collection ها تجزیه پذیری خوبی ندارند و در پایین انها را مقایسه میکنیم :
تجزیه پذیری عالی : ArrayList , IntStream.range
تجزیه پذیری خوب : HashSet , TreeSet
تجزیه پذیری ضعیف : LinkedList , Stream.iterate
نکته ای که وجود دارد این است که حتی در مواردی که از ArrayList یک stream گرفته باشیم استفاده از parallel آیا کار ما را سریعتر میکند ؟ همیشه نه و باید دقت کنیم که اگر عناصر موجود تعدادشان کم باشد تاثیر معکوسی در پرفرمانس خواهد داشت
و یا موردی که از یک collection با تجزیه پذیری ضعیف استفاده کرده باشیم مانند LinkedList آیا استفاده از parallel پرفرمانس را کاهش میدهد همیشه ؟ باز بستگی دارد اگر عملیات ما بسیار وقتگیر بوده باشد شکستن LinkedList به چند stream و انجام عملیات همروند میتواند پرفرمانس ما را افزایش دهد
و بهترین کار استفاده از بنچمارک های دستی است
مثالی برای دیدن نتایج اشتباه با استفاده از parallel در stream :
class Accumulator { long total=0; public void add(long value) { total+=value; } } long sideEffectParallelSum(long n) { Accumulator acc = new Accumulator(); LongStream.rangeClosed(1 , n) .parallel().forEach(acc::add); return acc.total; }
متد sequential : برعکس متد parallel عملیات را از حالت چند تردی خارج میکند
متد map : تک تک اعضای دنباله را به نوعی دیگر تبدیل میکند پارامتر ورودی map از نوع Function است و درون Function ما میتوانیم نوع ورودی و نوع خروجی را مشخص کنیم
List<Car> list = ...; list.stream().map (car -> car.color) list.stream() .map(car -> car.color) .filter(color -> color.startsWith("B")) .forEach(System.out::println);
در کد بالا ابتدا ما Stream از نوع Car داشتیم که تبدیل شده با Stream از نوع String که رنگ ماشین ها را نگهداری میکند
متد reduce : یک عملیات terminal است که یک مقدار تجمیع شده را بر میگرداند که پارامتر ورودی آن یک BinaryOperator است که دو آرگومان میگیرد و آن دو را باهم ترکیب و یک مقدار واحد از نوع Optional بر میگرداند
چرا خروجی از نوع Optional است ؟ چون لیست ما میتواند بدون عضو باشد
Optional<Integer> sum = list.stream() .map(car -> car.price) .reduce((price1 , price2) -> price1 + price2); sum.ifPresent(System.out::prinln);
متد count : یک عملیات terminal است و تعداد اعضای دنباله را بصورت یک long برمیگرداند
متد collect : یک عملیات terminal است که یک stream را به یکی از انواه Collection ها تبدیل میکند و یک ورودی از نوع Collectors میگیرد
List<Car> newList = list.stream() .filter((a) -> a.price < 50) .collect(Collectors.toList()); Set<Car> newSet = list.stream() .filter((a) -> a.price < 50) .collect(Collectors.toSet()); Map<String,Car> newMap = list.stream() .filter((a) -> a.price < 50) .collect(Collectors.toMap(car -> car.color , car -> car));
در کد بالا در نوع تبدیل به Map مقدار ورودی Collectors دو پارامتر از نوع Function را گرفته است
متد anyMatch : چک میکند که آیا حداقل یک شرط Predicate در بین اعضای دنباله وجود دارد یا خیر و مقدار boolean بر میگرداند
متد allMatch : چک میکند که آیا در کل اعضای دنباله شرط Predicate وجود داشته باشد و مقدار boolean بر میگرداند
متد noneMatch : چک میکند که آیا در کل اعضای دنباله شرط Predicate وجود نداشته باشد و مقدار boolean بر میگرداند
boolean anyBlack = list.stream() .anyMatch(car -> car.color.equals("Black")); boolean allBlack = list.stream() .allMatch(car -> car.color.equals("Black")); boolean noneBlack = list.stream() .noneMatch(car -> car.color.equals("Black"));
راه های دیگری برای ایجاد stream های اعداد وجود دارند که بعضا پرفرمانس بالاتری هم دارند
IntStream oneTo19 = IntStream.range(1 , 20); IntStream oneTo20 = IntStream.rangeClosed(1 , 20);
ایجاد stream ای از اعداد با کمک iterator :
Stream.iterator(0, x -> x + 2) .limit(10) .forEach(System.out::println);
پارامتر اول در iterator مبدا شروع و پارامتر دوم یک UnaryOperator است که نجوه ایجاد عضو بعدی از روی عضو قبلی را مشخص کرده است و میتواند بینهایت عضو بسازد و باید بلافاصله با متد limit آنرا محدود به تعداد خاصی کنیم
ساخت stream با استفاده از of :
Stream<String> stream = Stream.of("a","b","c");
ساخت stream با استفاده از Arrays :
String [] arr = {"a","b","c"}; Stream<String> stream = Arrays.stream(arr);
ساخت stream روی فایل :
Stream<String> stream = Files.lines(Paths.get("file.txt"));