Bu məqalə kimin üçündür?
- Java Core-u artıq yaxşı bildiyini düşünən, lakin Java-da lambda ifadələri haqqında heç bir fikri olmayanlar üçün. Və ya, bəlkə də, siz artıq lambdalar haqqında bir şey eşitmisiniz, lakin təfərrüatlar olmadan.
- lambda ifadələri haqqında bir qədər anlayışı olan, lakin hələ də onlardan istifadə etməkdən qorxan və qeyri-adi olanlar üçün.
Bu kateqoriyalardan birinə aid deyilsinizsə, bu məqaləni cansıxıcı, səhv və ümumiyyətlə “sərin deyil” tapa bilərsiniz. Bu halda, ya keçməkdən çəkinməyin, ya da mövzunu yaxşı bilirsinizsə, şərhlərdə məqaləni necə təkmilləşdirə və ya əlavə edə biləcəyimi təklif edin. Material heç bir akademik dəyər iddia etmir, daha az yenilikdir. Əksinə, əksinə: mən burada mürəkkəb (bəziləri üçün) şeyi mümkün qədər sadə təsvir etməyə çalışacağam. Mən axın api izah etmək xahişi ilə yazmağa ilham verdim. Mən bu barədə düşündüm və lambda ifadələrini başa düşmədən "axınlar" haqqında bəzi nümunələrimin anlaşılmaz olacağına qərar verdim. Beləliklə, lambdalardan başlayaq.
Bu məqaləni başa düşmək üçün hansı bilik tələb olunur:
- Obyekt yönümlü proqramlaşdırma (bundan sonra OOP adlandırılacaq), yəni:
- siniflərin və obyektlərin nə olduğunu, onlar arasındakı fərqin nə olduğunu bilmək;
- interfeyslərin nə olduğunu, onların siniflərdən nə ilə fərqləndiyini, onlar arasında hansı əlaqənin olduğunu (interfeyslər və siniflər) bilmək;
- metodun nə olduğunu, onun necə adlandırılacağını, mücərrəd metodun (yaxud icrası olmayan metod) nə olduğunu, metodun parametrlərinin/arqumentlərinin nə olduğunu, onları ora necə ötürməli olduğunu bilmək;
- giriş modifikatorları, statik metodlar/dəyişənlər, yekun metodlar/dəyişənlər;
- irsilik (siniflər, interfeyslər, interfeyslərin çoxlu irsiyyəti).
- Java Core bilikləri: generiklər, kolleksiyalar (siyahılar), mövzular.
Yaxşı, başlayaq.
Bir az tarix
Lambda ifadələri Java-ya funksional proqramlaşdırmadan, orada isə riyaziyyatdan gəldi. 20-ci əsrin ortalarında Amerikada Princeton Universitetində riyaziyyatı və hər cür abstraksiyanı çox sevən müəyyən bir Alonzo Kilsəsi işləyirdi. Əvvəlcə bəzi mücərrəd ideyalar toplusu olan və proqramlaşdırma ilə heç bir əlaqəsi olmayan lambda hesabı ilə çıxış edən Alonzo Kilsəsi idi. Eyni zamanda, Alan Turing və John von Neumann kimi riyaziyyatçılar eyni Princeton Universitetində çalışırdılar. Hər şey bir araya gəldi: Church lambda hesablama sistemi ilə gəldi, Türinq indi "Türinq maşını" kimi tanınan mücərrəd hesablama maşınını inkişaf etdirdi. Yaxşı, fon Neumann müasir kompüterlərin əsasını təşkil edən kompüterlərin arxitekturasının diaqramını təklif etdi (və indi "von Neumann memarlığı" adlanır). O dövrdə Alonzo Kilsənin ideyaları həmkarlarının işi ("saf" riyaziyyat sahəsi istisna olmaqla) qədər şöhrət qazanmamışdı. Ancaq bir az sonra müəyyən bir Con Makkarti (həmçinin, hekayə zamanı Prinston Universitetinin məzunu - Massaçusets Texnologiya İnstitutunun əməkdaşı) Churchun fikirləri ilə maraqlandı. Onların əsasında 1958-ci ildə ilk funksional proqramlaşdırma dili olan Lisp-i yaratdı. 58 il sonra isə funksional proqramlaşdırma ideyaları Java-ya 8 rəqəmi kimi sızdı. Aradan 70 il belə keçmədi... Əslində bu, riyazi ideyanın praktikada tətbiqi üçün ən uzun müddət deyil.
mahiyyəti
Lambda ifadəsi belə bir funksiyadır. Bunu Java-da adi metod kimi düşünə bilərsiniz, yeganə fərq odur ki, arqument kimi digər metodlara ötürülə bilər. Bəli, üsullara təkcə rəqəmləri, simləri və pişikləri deyil, digər üsulları da ötürmək mümkün oldu! Buna nə vaxt ehtiyacımız ola bilər? Məsələn, bəzi geri çağırışlardan keçmək istəsək. Bizə zəng etdiyimiz metod lazımdır ki, ona keçdiyimiz başqa bir metodu çağıra bilək. Yəni, bəzi hallarda bir geri çağırışı, digərlərində isə digərini ötürmək imkanımız olsun. Geri çağırışlarımızı qəbul edən metodumuz onları çağıracaq. Sadə bir nümunə çeşidləmədir. Tutaq ki, biz bu kimi görünən bir növ çətin çeşidləmə yazırıq:
public void mySuperSort() {
if(compare(obj1, obj2) > 0)
}
Harada,
if
metodu adlandırırıq
compare()
, ora müqayisə etdiyimiz iki obyekti veririk və bu obyektlərdən hansının “böyük” olduğunu öyrənmək istəyirik. "Daha çox" olanı "kiçik" olandan əvvəl qoyacağıq. Mən dırnaq içərisində “daha çox” yazdım, çünki biz təkcə artan deyil, həm də azalan qaydada çeşidləyə biləcək universal bir üsul yazırıq (bu halda “daha çox” mahiyyətcə daha kiçik olan obyekt olacaq və əksinə) . Düzgün çeşidləmək istədiyimiz qaydanı təyin etmək üçün onu birtəhər özümüzə ötürməliyik
mySuperSort()
. Bu halda, metodumuzu çağırarkən bir şəkildə "nəzarət" edə biləcəyik. Əlbəttə ki, artan və azalan qaydada çeşidləmə
mySuperSortAsc()
üçün iki ayrı üsul yaza bilərsiniz .
mySuperSortDesc()
Və ya metodun daxilində bəzi parametrləri keçirin (məsələn,
boolean
əgər
true
, artan qaydada və əgər
false
azalan qaydada olarsa). Bəs bəzi sadə strukturları deyil, məsələn, sətir massivlərinin siyahısını çeşidləmək istəsək nə olar? Metodumuz
mySuperSort()
bu sətir massivlərinin necə çeşidlənməsini necə biləcək? Ölçü üçün? Sözlərin ümumi uzunluğuna görə? Bəlkə də serialın birinci cərgəsindən asılı olaraq əlifba sırası ilə? Bəs bəzi hallarda massivlərin siyahısını massivin ölçüsünə, digər halda isə massivdəki sözlərin ümumi uzunluğuna görə çeşidləmək lazım gələrsə, necə? Düşünürəm ki, siz artıq müqayisələr haqqında eşitmisiniz və belə hallarda biz sadəcə olaraq müqayisə obyektini çeşidləmək istədiyimiz qaydaları təsvir etdiyimiz çeşidləmə metodumuza keçirik. Standart metod
sort()
eyni prinsiplə həyata keçirildiyi üçün
mySuperSort()
nümunələrdə standart metoddan istifadə edəcəyəm
sort()
.
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};
List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);
Comparator<String[]> sortByLength = new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
};
Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
int length1 = 0;
int length2 = 0;
for (String s : o1) {
length1 += s.length();
}
for (String s : o2) {
length2 += s.length();
}
return length1 - length2;
}
};
arrays.sort(sortByLength);
Nəticə:
- Ana çərçivəni yudu
- sülh Əmək bilər
- Mən java-nı həqiqətən sevirəm
Burada massivlər hər massivdəki sözlərin sayına görə sıralanır. Daha az sözdən ibarət massiv “kiçik” hesab olunur. Buna görə də başlanğıcda gəlir. Daha çox söz olan söz "daha çox" sayılır və sonunda bitir. Əgər metoda
sort()
başqa bir müqayisə aparsaq
(sortByWordsLength)
,
nəticə fərqli olacaq:
- sülh Əmək bilər
- Ana çərçivəni yudu
- Mən java-nı həqiqətən sevirəm
İndi massivlər belə massivin sözlərindəki hərflərin ümumi sayına görə sıralanır. Birinci halda 10 hərf, ikincidə 12, üçüncüdə isə 15 hərf var. Əgər biz yalnız bir müqayisəçidən istifadə etsək, onda biz onun üçün ayrıca dəyişən yarada bilmərik, sadəcə olaraq anonim sinif obyekti yarada bilərik. metodu çağırma vaxtı
sort()
. Bunun kimi:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};
List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);
arrays.sort(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
});
Nəticə birinci halda olduğu kimi olacaq. Tapşırıq 1 . Bu nümunəni yenidən yazın ki, o, massivləri massivdəki sözlərin sayına görə artan qaydada deyil, azalan ardıcıllıqla çeşidləsin. Biz bütün bunları artıq bilirik. Biz obyektləri metodlara necə ötürəcəyimizi bilirik, bu və ya digər obyekti hal-hazırda ehtiyacımızdan asılı olaraq metoda ötürə bilərik və belə bir obyekti keçdiyimiz metodun içərisində həyata keçirməyi yazdığımız üsul adlanacaq. . Sual yaranır: lambda ifadələrinin bununla nə əlaqəsi var?
Nəzərə alsaq ki, lambda məhz bir metodu ehtiva edən obyektdir. Metod obyekti kimidir. Bir obyektə bükülmüş üsul. Onların bir az qeyri-adi sintaksisi var (lakin bu barədə daha sonra). Bu girişə bir daha nəzər salaq
arrays.sort(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
});
Burada siyahımızı götürürük
arrays
və onun metodunu çağırırıq
sort()
, burada müqayisə obyektini bir metodla keçirik
compare()
(nə adlanması bizim üçün fərqi yoxdur, çünki bu obyektdə yeganədir, onu qaçırmayacağıq). Bu üsul iki parametr götürür, biz bundan sonra işləyəcəyik.
Əgər siz IntelliJ IDEA- da işləyirsinizsə , yəqin ki, onun sizə bu kodu əhəmiyyətli dərəcədə qısaltmaq üçün necə təklif etdiyini görmüsünüz:
arrays.sort((o1, o2) -> o1.length - o2.length);
Beləliklə, altı sətir bir qısa sətirə çevrildi. 6 sətir bir qısa sətirə yenidən yazılmışdır. Nəsə itdi, amma zəmanət verirəm ki, heç bir vacib heç nə yoxa çıxmayıb və bu kod anonim siniflə eyni şəkildə işləyəcək.
Tapşırıq 2 . Lambdalardan istifadə edərək 1-ci problemin həllini necə yenidən yazacağınızı anlayın (son çarə olaraq
IntelliJ IDEA-dan anonim sinifinizi lambdaya çevirməsini xahiş edin).
İnterfeyslər haqqında danışaq
Əsasən, interfeys yalnız mücərrəd metodların siyahısıdır. Biz bir sinif yaratdıqda və onun bir növ interfeys həyata keçirəcəyini söylədikdə, biz sinifimizdə interfeysdə sadalanan metodların tətbiqini yazmalıyıq (yaxud son çarə olaraq onu yazmamalı, sinfi mücərrəd etməliyik. ). Çox müxtəlif üsullarla interfeyslər var (məsələn
List
), yalnız bir metodu olan interfeyslər var (məsələn, eyni Müqayisə edən və ya İşlənə bilən). Heç bir metodu olmayan interfeyslər var (marker interfeysləri adlanır, məsələn Serializable). Yalnız bir metodu olan interfeyslərə
funksional interfeyslər də deyilir .
Java 8-də onlar hətta xüsusi @FunctionalInterface annotasiyası ilə qeyd olunurlar . Bu, lambda ifadələri ilə istifadə üçün uyğun olan bir metodla interfeyslərdir. Yuxarıda dediyim kimi, lambda ifadəsi obyektə bükülmüş bir üsuldur. Və belə bir obyektin yanından keçəndə, əslində, bu tək metoddan keçirik. Məlum oldu ki, bu metodun necə adlandırılması bizim üçün heç bir əhəmiyyət kəsb etmir. Bizim üçün vacib olan bu metodun qəbul etdiyi parametrlər və əslində metod kodunun özüdür. Bir lambda ifadəsi, əslində. funksional interfeysin həyata keçirilməsi. Bir metodla interfeys gördüyümüz yerdə bu, lambdadan istifadə edərək belə anonim sinfi yenidən yaza biləcəyimiz deməkdir. Əgər interfeysdə birdən çox/az metod varsa, o zaman lambda ifadəsi bizə uyğun gəlməyəcək və biz anonim sinifdən, hətta adi sinifdən istifadə edəcəyik. Lambdaları qazmağın vaxtı gəldi. :)
Sintaksis
Ümumi sintaksis belədir:
(параметры) -> {тело метода}
Yəni, mötərizə, onların içərisində metod parametrləri, bir "ox" (bunlar ard-arda iki simvoldur: mənfi və daha böyük), bundan sonra metodun gövdəsi həmişə olduğu kimi qıvrımlı mötərizədədir. Parametrlər metodu təsvir edərkən interfeysdə göstərilənlərə uyğun gəlir. Dəyişənlərin tipi kompilyator tərəfindən aydın şəkildə müəyyən edilə bilərsə (bizim vəziyyətimizdə sətir massivləri ilə işlədiyimiz dəqiq məlumdur, çünki o,
List
sətir massivləri ilə dəqiq şəkildə yazılmışdır), onda dəyişənlərin tipi
String[]
lazım deyil. yazılsın.
Əmin deyilsinizsə, növü göstərin və ehtiyac olmadıqda IDEA onu boz rəngdə vurğulayacaq. |
Məsələn,
Oracle təlimatında daha çox oxuya bilərsiniz .
Buna "hədəf yazmaq" deyilir . Siz dəyişənlərə hər hansı ad verə bilərsiniz, mütləq interfeysdə göstərilənləri deyil. Parametrlər yoxdursa, sadəcə mötərizələr. Yalnız bir parametr varsa, sadəcə mötərizəsiz dəyişənin adı. Parametrləri sıraladıq, indi lambda ifadəsinin özü haqqında. Buruq mötərizələrin içərisinə kodu adi üsul kimi yazın. Bütün kodunuz yalnız bir sətirdən ibarətdirsə, siz ümumiyyətlə əyri mötərizələr yazmağa ehtiyac yoxdur (ifs və döngələrdə olduğu kimi). Əgər lambdanız bir şey qaytarırsa, lakin onun gövdəsi bir sətirdən ibarətdirsə,
return
yazmağa heç ehtiyac yoxdur. Ancaq buruq mötərizələriniz varsa, adi üsulda olduğu kimi, açıq şəkildə yazmalısınız
return
.
Nümunələr
Misal 1.
() -> {}
Ən sadə variant. Və ən mənasızı :).Çünki heç nə etmir.
Misal 2.
() -> ""
Həm də maraqlı bir seçim. O, heç nə qəbul etmir və boş sətir qaytarır (
return
lazımsız kimi buraxılıb). Eyni, lakin ilə
return
:
() -> {
return "";
}
Nümunə 3. Lambdalardan istifadə edən salam dünya
() -> System.out.println("Hello world!")
return
Heç nə qəbul etmir, heç nə qaytarmır ( zəngdən əvvəl qoya bilmərik
System.out.println()
, çünki metodda qaytarma növü
println() — void)
sadəcə olaraq ekranda yazı göstərir. İnterfeys həyata keçirmək üçün idealdır
Runnable
. Eyni nümunə daha dolğundur:
public class Main {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello world!")).start();
}
}
Və ya bu kimi:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> System.out.println("Hello world!"));
t.start();
}
}
Runnable
Və ya hətta lambda ifadəsini tipli obyekt kimi saxlaya və sonra onu konstruktora ötürə bilərik
thread’а
:
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("Hello world!");
Thread t = new Thread(runnable);
t.start();
}
}
Lambda ifadəsinin dəyişənə saxlanma anına daha yaxından nəzər salaq. İnterfeys
Runnable
bizə deyir ki, onun obyektlərinin metodu olmalıdır
public void run()
. İnterfeys görə, run metodu heç nəyi parametr kimi qəbul etmir. Və heç nə qaytarmır
(void)
. Buna görə də, bu şəkildə yazarkən, heç nə qəbul etməyən və ya qaytarmayan hansısa üsulla obyekt yaradılacaq.
run()
Hansı ki, interfeysdəki üsulla olduqca uyğundur
Runnable
. Buna görə də biz bu lambda ifadəsini kimi dəyişənə qoya bildik
Runnable
.
Misal 4
() -> 42
Yenə də heç nə qəbul etmir, lakin 42 rəqəmini qaytarır. Bu lambda ifadəsi tipli dəyişəndə yerləşdirilə bilər
Callable
, çünki bu interfeys yalnız bir metodu müəyyən edir, bu da belə görünür:
V call(),
V
qayıdış dəyərinin növü haradadır (bizim vəziyyətimizdə )
int
. Müvafiq olaraq, belə bir lambda ifadəsini aşağıdakı kimi saxlaya bilərik:
Callable<Integer> c = () -> 42;
Misal 5. Bir neçə sətirdə lambda
() -> {
String[] helloWorld = {"Hello", "world!"};
System.out.println(helloWorld[0]);
System.out.println(helloWorld[1]);
}
Yenə də bu, parametrləri və qaytarma növü olmayan lambda ifadəsidir
void
(çünki yoxdur
return
).
Misal 6
x -> x
Burada nəyisə dəyişənə daxil edirik
х
və onu qaytarırıq. Nəzərə alın ki, yalnız bir parametr qəbul edilərsə, onun ətrafındakı mötərizələrin yazılmasına ehtiyac yoxdur. Eyni, lakin mötərizələrlə:
(x) -> x
Və burada açıq bir seçim var
return
:
x -> {
return x;
}
Və ya bu kimi, mötərizədə və
return
:
(x) -> {
return x;
}
Və ya növün açıq bir göstəricisi ilə (və müvafiq olaraq mötərizə ilə):
(int x) -> x
Misal 7
x -> ++x
Biz onu qəbul edirik
х
və geri qaytarırıq, amma
1
daha çoxu üçün. Siz həmçinin bu şəkildə yenidən yaza bilərsiniz:
x -> x + 1
Hər iki halda biz parametr, metod gövdəsi və söz ətrafında mötərizə göstərmirik
return
, çünki bu lazım deyil. Mötərizədə və qaytarma ilə seçimlər nümunə 6-da təsvir edilmişdir.
Misal 8
(x, y) -> x % y
Bəzilərini qəbul edirik
х
və
у
bölmənin qalan hissəsini
x
ilə qaytarırıq
y
. Parametrlər ətrafında mötərizələr artıq burada tələb olunur. Yalnız bir parametr olduqda onlar isteğe bağlıdır. Tiplərin açıq şəkildə göstərildiyi kimi:
(double x, int y) -> x % y
Misal 9
(Cat cat, String name, int age) -> {
cat.setName(name);
cat.setAge(age);
}
Biz Cat obyektini, adı və tam yaşı olan sətri qəbul edirik. Metodun özündə biz keçmiş adı və yaşı Pişikə təyin etdik. Dəyişənimiz istinad növü olduğundan, lambda ifadəsinin xaricindəki Cat obyekti dəyişəcək (o, daxilə ötürülən adı
cat
və yaşı alacaq). Bənzər bir lambda istifadə edən bir az daha mürəkkəb versiya:
public class Main {
public static void main(String[] args) {
Cat myCat = new Cat();
System.out.println(myCat);
Settable<Cat> s = (obj, name, age) -> {
obj.setName(name);
obj.setAge(age);
};
changeEntity(myCat, s);
System.out.println(myCat);
}
private static <T extends WithNameAndAge> void changeEntity(T entity, Settable<T> s) {
s.set(entity, "Murzik", 3);
}
}
interface WithNameAndAge {
void setName(String name);
void setAge(int age);
}
interface Settable<C extends WithNameAndAge> {
void set(C entity, String name, int age);
}
class Cat implements WithNameAndAge {
private String name;
private int age;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Nəticə: Cat{name='null', age=0} Cat{name='Murzik', age=3} Gördüyünüz kimi əvvəlcə Cat obyektinin bir vəziyyəti var idi, lakin lambda ifadəsindən istifadə etdikdən sonra vəziyyət dəyişdi . Lambda ifadələri generiklərlə yaxşı işləyir.
Dog
Və əgər məsələn, onu da həyata keçirəcək bir sinif yaratmalıyıqsa
WithNameAndAge
, o zaman metodda
main()
lambda ifadəsinin özünü dəyişdirmədən eyni əməliyyatları Dog ilə edə bilərik.
Tapşırıq 3 . Rəqəm götürən və Boolean dəyərini qaytaran metodla funksional interfeys yazın. Bu cür interfeysin icrasını lambda ifadəsi şəklində yazın,
true
əgər ötürülən ədəd 13-ə qalıqsız bölünürsə , onu qaytarın.Tapşırıq
4 . İki sətir götürən və eyni sətri qaytaran metodla funksional interfeys yazın. Ən uzun sətri qaytaran lambda şəklində belə bir interfeysin həyata keçirilməsini yazın.
Tapşırıq 5 . Üç fraksiyalı ədədi qəbul edən metodla funksional interfeys yazın:
a
,
b
,
c
və eyni kəsr ədədini qaytarır. Diskriminant qaytaran lambda ifadəsi şəklində belə bir interfeysin həyata keçirilməsini yazın. Kim unutdu,
D = b^2 - 4ac .
Tapşırıq 6 . 5-ci tapşırığın funksional interfeysindən istifadə edərək, əməliyyatın nəticəsini qaytaran lambda ifadəsini yazın
a * b^c
.
Java-da lambda ifadələri haqqında məşhurdur. Nümunələr və tapşırıqlarla. 2-ci hissə.
GO TO FULL VERSION