جاوا و تکنولوژی های آن

java programming language

در این وبلاگ به بررسی نکات موجود در جاوا و تکنولوژی های آن می پردازیم

طبقه بندی موضوعی

۱ مطلب با کلمه‌ی کلیدی «Function» ثبت شده است

جاوا در نسخه 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 : تابعی است که یک ورودی میگیرد و یک خروجی تولید میکند و با متد apply کار میکند و با متد andThen میتوان Function های مختلفی را باهم ترکیب کرد در حقیقت میتوانیم ببینیم که Predicate یک نوع خاص تری از Function است که یک ورودی میگرفت و تنها boolean بر میگرداند ولی در Function محدودیتی در نوع خروجی نداریم 
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);
عبارت داخل filter در حقیقت یک Predicate است 
اگر اعضای دنباله از نوع  Comparable باشند نیازی نیست که درون sort را پر کنیم 
متد limit تنها بر روی تعداد خاصی از اعضای ابتدایی تا تعداد مشخص شده را در نظر میگیرد و بقیه را نادیده میگیرد 
متد skip تعداد خاصی از اعضای مشخص شده از ابتدای دنباله را در نادیده میگیرد و بقیه مورد استفاده قرار میگیرد

متد 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;
}
در کد بالا ترد های مختلفی متد add را فراخوانی میکنند و قبل از اینکه کار متد add پایان بپذیرد متد دیگری مقدار ورودی را به total اضافه کرده و اینطوری جوابی که در خروجی خواهیم داشت صحیح نخواهد بود و باید متد add بصورت synchronized تعریف میشد که در آن صورت باز استفاده از parallel تاثیری نخواهد داشت 

متد 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"));
متد distinct : اعضای یونیک موجود در دنباله را در قالب یک stream بر میگرداند
متد findAny : یکی از اعضای دنباله را بصورت تصادفی در قالب Optional برمیگرداند
متد findFirst : اولین عضو دنباله را در قالب Optional برمیگرداند
متد max : عضوی با بزرگترین مقدار از دنباله را بر میگرداند
متد min : عضوی با کمترین مقدار از دنباله را بر میگرداند
متد toArray : دنباله های موجود را در قالب یک ارایه برمیگرداند 



راه های دیگری برای ایجاد 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"));