JavaRush /Java Blogu /Random-AZ /ArrayList.forEach-də lambdalar və metod istinadları - nec...

ArrayList.forEach-də lambdalar və metod istinadları - necə işləyir

Qrupda dərc edilmişdir
Java Syntax Zero axtarışında lambda ifadələrinə giriş çox konkret bir nümunə ilə başlayır:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Mühazirə müəllifləri ArrayList sinfinin standart forEach funksiyasından istifadə edərək lambdaları və metod istinadlarını təhlil edirlər. Şəxsən mən baş verənlərin mənasını başa düşməkdə çətinlik çəkdim, çünki bu funksiyanın həyata keçirilməsi, eləcə də onunla əlaqəli interfeys "başlıq altında" qalır. Arqument (lər)in haradan gəldiyi , println() funksiyasının haradan ötürülməsi özümüz cavab verməli olacağımız suallardır. Xoşbəxtlikdən, IntelliJ IDEA ilə biz asanlıqla ArrayList sinifinin daxili hissələrinə baxa və bu əriştəni əvvəldən aça bilərik. Əgər siz də heç nə başa düşmürsünüzsə və anlamaq istəyirsinizsə, bu işdə sizə az da olsa kömək etməyə çalışacağam. Lambda ifadəsi və ArrayList.forEach - necə işləyir Mühazirədən biz artıq bilirik ki, lambda ifadəsi funksional interfeysin həyata keçirilməsidir . Yəni, biz bir funksiya ilə interfeys elan edirik və bu funksiyanın nə etdiyini təsvir etmək üçün lambdadan istifadə edirik. Bunun üçün sizə lazımdır: 1. Funksional interfeys yaratmaq; 2. Tipi funksional interfeysə uyğun dəyişən yaradın; 3. Bu dəyişənə funksiyanın icrasını təsvir edən lambda ifadəsini təyin edin; 4. Bir dəyişənə daxil olaraq funksiyaya zəng edin (bəlkə də terminologiyada kobud danışıram, lakin bu, ən aydın yoldur). Google-dan sadə bir nümunə verəcəyəm, ona ətraflı şərhlər verəcəyəm (metanit.com saytının müəlliflərinə təşəkkür edirəm):
interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс,
    // который можно реализовать с помощью лямбды
}

public class LambdaApp {

    public static void main(String[] args) {

        // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
        Operationable operation;
        // Прописываем реализацию функции calculate с помощью лямбды, на вход подаём x и y, на выходе возвращаем их сумму
        operation = (x,y)->x+y;

        // Теперь мы можем обратиться к функции calculate через переменную operation
        int result = operation.calculate(10, 20);
        System.out.println(result); //30
    }
}
İndi mühazirədən nümunəyə qayıdaq. Siyahı kolleksiyasına String tipli bir neçə element əlavə olunur . Elementlər daha sonra siyahı obyektində çağırılan standart forEach funksiyasından istifadə etməklə götürülür . Bəzi qəribə s parametrli lambda ifadəsi funksiyaya arqument kimi ötürülür .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Əgər burada nə baş verdiyini dərhal başa düşməmisinizsə, deməli tək deyilsiniz. Xoşbəxtlikdən, IntelliJ IDEA-nın əla klaviatura qısa yolu var: Ctrl+Left_Mouse_Button . Əgər biz forEach-in üzərinə sürüb bu kombinasiyaya klik etsək, standart ArrayList sinifinin mənbə kodu açılacaq, burada forEach metodunun tətbiqini görəcəyik :
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i));
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
Giriş arqumentinin Consumer tipli hərəkət olduğunu görürük . Kursoru Consumer sözünün üzərinə aparaq və yenidən Ctrl+LMB sehrli kombinasiyasını sıxırıq . İstehlakçı interfeysinin təsviri açılacaq . Defolt tətbiqi ondan çıxarsaq (indi bizim üçün vacib deyil), aşağıdakı kodu görəcəyik:
public interface Consumer<t> {
   void accept(T t);
}
Belə ki. İstənilən növdən bir arqumenti qəbul edən tək qəbul funksiyası olan İstehlakçı interfeysimiz var . Yalnız bir funksiya olduğundan, interfeys funksionaldır və onun həyata keçirilməsi lambda ifadəsi ilə yazıla bilər. Artıq gördük ki, ArrayList-in fəaliyyət arqumenti kimi İstehlakçı interfeysinin həyata keçirilməsini qəbul edən forEach funksiyası var . Bundan əlavə, forEach funksiyasında biz aşağıdakı kodu tapırıq:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
For döngəsi əsasən ArrayList-in bütün elementləri arasında təkrarlanır. Döngənin içərisində biz fəaliyyət obyektinin qəbul funksiyasına çağırış görürük - xatırlayırsınız ki, biz əməliyyat.hesabla necə çağırırıq? Kolleksiyanın cari elementi qəbul funksiyasına ötürülür . İndi biz nəhayət orijinal lambda ifadəsinə qayıda bilərik və onun nə etdiyini başa düşə bilərik. Bütün kodu bir yığında toplayaq:
public interface Consumer<t> {
   void accept(T t); // Функция, которую мы реализуем лямбда-выражением
}

public void forEach(Consumer<? super E> action) // В action хранится an object Consumer, в котором функция accept реализована нашей лямбдой {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i)); // Вызываем нашу реализацию функции accept интерфейса Consumer для каждого element коллекции
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

//...

list.forEach( (s) -> System.out.println(s) );
Bizim lambda ifadəmiz İstehlakçı interfeysində təsvir edilən qəbul funksiyasının həyata keçirilməsidir . Lambdadan istifadə edərək, qəbul funksiyasının s arqumentini qəbul etdiyini və onu ekranda göstərdiyini qeyd etdik. Lambda ifadəsi İstehlakçı interfeysinin həyata keçirilməsini saxlayan fəaliyyət arqumenti kimi forEach funksiyasına ötürüldü . İndi forEach funksiyası bizim İstehlakçı interfeysinin tətbiqini belə bir xətt ilə çağıra bilər:
action.accept(elementAt(es, i));
Beləliklə, lambda ifadəsindəki giriş arqumenti s ArrayList kolleksiyasının digər elementidir və bu bizim İstehlakçı interfeysinin tətbiqinə ötürülür . Hamısı budur: biz ArrayList.forEach-də lambda ifadəsinin məntiqini təhlil etdik. ArrayList.forEach-də metoda istinad - bu necə işləyir? Mühazirədə növbəti addım metod istinadlarına baxmaqdır. Düzdür, onlar bunu çox qəribə şəkildə başa düşürlər - mühazirəni oxuduqdan sonra bu kodun nə etdiyini başa düşmək şansım yox idi:
list.forEach( System.out::println );
Birincisi, yenə bir az nəzəriyyə. Metod arayışı , çox kobud desək, başqa bir funksiya tərəfindən təsvir edilən funksional interfeysin həyata keçirilməsidir . Yenə də sadə bir nümunə ilə başlayacağam:
public interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс
}

public static class Calculator {
    // Создадим статический класс Calculator и пропишем в нём метод methodReference.
    // Именно он будет реализовывать функцию calculate из интерфейса Operationable.
    public static int methodReference(int x, int y) {
        return x+y;
    }
}

public static void main(String[] args) {
    // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
    Operationable operation;
    // Теперь реализацией интерфейса будет не лямбда-выражение, а метод methodReference из нашего класса Calculator
    operation = Calculator::methodReference;

    // Теперь мы можем обратиться к функции интерфейса через переменную operation
    int result = operation.calculate(10, 20);
    System.out.println(result); //30
}
Mühazirədən nümunəyə qayıdaq:
list.forEach( System.out::println );
Nəzərinizə çatdırım ki, System.out println funksiyasına malik PrintStream tipli obyektdir . Gəlin imlecini println üzərinə aparaq və Ctrl+LMB klikləyin :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
İki əsas xüsusiyyəti qeyd edək: 1. Println funksiyası heç nə qaytarmır (boş). 2. println funksiyası giriş kimi bir arqument qəbul edir. Sizə heç nəyi xatırlatmır?
public interface Consumer<t> {
   void accept(T t);
}
Düzdü - qəbul funksiyası imzası println metodu imzasının daha ümumi halıdır ! Bu o deməkdir ki, sonuncu bir metoda istinad kimi uğurla istifadə edilə bilər - yəni println qəbul funksiyasının xüsusi həyata keçirilməsinə çevrilir :
list.forEach( System.out::println );
Biz System.out obyektinin println funksiyasını forEach funksiyasına arqument kimi ötürdük . Prinsip lambda ilə eynidir: indi forEach action.accept(elementAt(es, i)) çağırışı vasitəsilə kolleksiya elementini println funksiyasına ötürə bilər . Əslində, bunu indi System.out.println(elementAt(es, i)) kimi oxumaq olar .
public void forEach(Consumer<? super E> action) // В action хранится an object Consumer, в котором функция accept реализована методом println {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        final Object[] es = elementData;
        final int size = this.size;
        for (int i = 0; modCount == expectedModCount && i < size; i++)
            action.accept(elementAt(es, i)); // Функция accept теперь реализована методом System.out.println!
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
Ümid edirəm ki, lambdalara və metod istinadlarına yeni başlayanlar üçün vəziyyəti bir az da olsa aydınlaşdırdım. Sonda Robert Schildtin məşhur "Java: Başlayanlar üçün Bələdçi" kitabını tövsiyə edirəm - mənim fikrimcə, orada lambdalar və funksiya istinadları olduqca həssas şəkildə təsvir edilmişdir.
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION