JavaRush /Java блогы /Random-KK /ArrayList.forEach ішіндегі ламбдалар және әдіс сілтемелер...
Anonymous #2633326
Деңгей

ArrayList.forEach ішіндегі ламбдалар және әдіс сілтемелері - ол қалай жұмыс істейді

Топта жарияланған
Java Syntax Zero квестіндегі лямбда өрнектеріне кіріспе өте нақты мысалдан басталады:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Дәріс авторлары ArrayList класының стандартты forEach функциясын пайдаланып ламбдалар мен әдіс сілтемелерін талдайды. Жеке өзіме не болып жатқанын түсіну қиынға соқты, өйткені бұл функцияны іске асыру, сонымен қатар онымен байланысты интерфейс «сорғыштың астында» қалады. Аргумент (лар) қайдан келеді , println() функциясы қайдан беріледі деген сұрақтарға өзіміз жауап беруіміз керек. Бақытымызға орай, IntelliJ IDEA көмегімен біз ArrayList класының ішкі бөліктерін оңай қарап шыға аламыз және бұл кеспенің басынан бастап шығара аламыз. Егер сіз де ештеңені түсінбесеңіз және оны түсінгіңіз келсе, мен сізге аз да болса көмектесуге тырысамын. Lambda өрнегі және ArrayList.forEach - бұл қалай жұмыс істейді Дәрістен біз ламбда өрнегі функционалды интерфейстің жүзеге асуы екенін білеміз . Яғни, біз бір функциямен интерфейсті жариялаймыз және бұл функцияның не істейтінін сипаттау үшін ламбданы қолданамыз. Ол үшін қажет: 1. Функционалдық интерфейсті құру; 2. Түрі функционалдық интерфейске сәйкес келетін айнымалыны құру; 3. Осы айнымалыға функцияның орындалуын сипаттайтын лямбда өрнегін тағайындаңыз; 4. Айнымалыға қол жеткізу арқылы функцияны шақырыңыз (мүмкін мен терминологияда дөрекі айтып отырған шығармын, бірақ бұл ең анық әдіс). Мен Google-дан қарапайым мысал келтіремін, оған егжей-тегжейлі түсініктемелер беремін (metanit.com сайтының авторларына рахмет):
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
    }
}
Енді лекциядағы мысалға оралайық. Тізім жиынына String түріндегі бірнеше элементтер қосылады . Содан кейін элементтер тізім нысанында шақырылатын стандартты forEach функциясының көмегімен шығарылады . Кейбір оғаш s параметрі бар лямбда өрнегі функцияға аргумент ретінде жіберіледі .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
Егер сіз мұнда не болғанын бірден түсінбесеңіз, онда сіз жалғыз емессіз. Бақытымызға орай, IntelliJ IDEA тамаша пернелер тіркесіміне ие: Ctrl+Left_Mouse_Button . Егер меңзерді forEach үстіне апарып, осы комбинацияны шертсек, стандартты ArrayList класының бастапқы codeы ашылады, онда біз forEach әдісінің орындалуын көреміз :
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();
}
Біз кіріс аргументі Consumer түріндегі әрекет екенін көреміз . Курсорды Consumer сөзінің үстіне жылжытып, Ctrl+LMB сиқырлы тіркесімін қайтадан басайық . Тұтынушы интерфейсінің сипаттамасы ашылады . Егер біз одан әдепкі енгізуді алып тастасақ (бұл қазір біз үшін маңызды емес), біз келесі codeты көреміз:
public interface Consumer<t> {
   void accept(T t);
}
Сонымен. Бізде кез келген түрдегі бір аргументті қабылдайтын жалғыз қабылдау функциясы бар Тұтынушы интерфейсі бар. Бір ғана функция болғандықтан, интерфейс функционалды болып табылады және оны жүзеге асыру ламбда өрнегі арқылы жазылуы мүмкін. ArrayList-те әрекет аргументі ретінде Тұтынушы интерфейсін іске асыруды қабылдайтын forEach функциясы бар екенін көрдік . Сонымен қатар, forEach функциясында біз келесі codeты табамыз:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
For циклі негізінен ArrayList-тің барлық элементтері арқылы қайталанады. Цикл ішінде біз әрекет нысанының қабылдау функциясына шақыруды көреміз - операция.есептеуді қалай атағанымызды есіңізде ме? Жинақтың ағымдағы элементі қабылдау функциясына беріледі . Енді біз бастапқы ламбда өрнегіне оралып, оның не істейтінін түсіне аламыз. Барлық codeты бір қадаға жинайық:
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) );
Біздің лямбда өрнегі Тұтынушы интерфейсінде сипатталған қабылдау функциясының орындалуы болып табылады . Ламбданы пайдаланып, қабылдау функциясы s аргументін қабылдайтынын және оны экранда көрсететінін көрсеттік . Lambda өрнегі forEach функциясына оның әрекет аргументі ретінде жіберілді , ол Тұтынушы интерфейсінің орындалуын сақтайды . Енді forEach функциясы біздің тұтынушы интерфейсін іске асыруды келесі жолмен шақыра алады:
action.accept(elementAt(es, i));
Осылайша, lambda өрнегіндегі s кіріс аргументі ArrayList жиынының басқа элементі болып табылады, ол Тұтынушы интерфейсін іске асыруға жіберіледі . Барлығы: ArrayList.forEach ішінде лямбда өрнегі логикасын талдадық. ArrayList.forEach ішіндегі әдіске сілтеме - ол қалай жұмыс істейді? Дәрістің келесі қадамы әдіс сілтемелерін қарау болып табылады. Рас, олар мұны өте біртүрлі түсінеді - лекцияны оқығаннан кейін мен бұл codeтың не істейтінін түсінуге мүмкіндігім болмады:
list.forEach( System.out::println );
Біріншіден, тағы да кішкене теория. Әдіс сілтемесі - бұл өте өрескел айтқанда, басқа функциямен сипатталған функционалды интерфейсті іске асыру . Тағы да қарапайым мысалдан бастайын:
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
}
Дәрістегі мысалға оралайық:
list.forEach( System.out::println );
Естеріңізге сала кетейін, System.out - бұл println функциясы бар PrintStream түріндегі an object . Меңзерді println үстіне апарып, Ctrl+LMB пернелерін басыңыз :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Екі негізгі мүмкіндікті атап өтейік: 1. println функциясы ештеңені қайтармайды (жарамсыз). 2. println функциясы кіріс ретінде бір аргумент алады. Сізге ештеңені еске түсірмейді ме?
public interface Consumer<t> {
   void accept(T t);
}
Бұл дұрыс - қабылдау функциясының қолтаңбасы println әдісі қолтаңбасының жалпы жағдайы болып табылады ! Бұл соңғысы әдіске сілтеме ретінде сәтті пайдаланылуы мүмкін дегенді білдіреді - яғни println қабылдау функциясының нақты іске асуына айналады :
list.forEach( System.out::println );
Біз System.out нысанының println функциясын forEach функциясына аргумент ретінде бердік . Принцип лямбдамен бірдей: енді forEach коллекция элементін println функциясына action.accept(elementAt(es, i)) шақыруы арқылы жібере алады . Шындығында, оны енді System.out.println(elementAt(es, i)) ретінде оқуға болады .
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();
    }
Мен ламбдалар мен әдіс сілтемелеріне жаңадан келгендер үшін жағдайды аздап түсіндірдім деп үміттенемін. Қорытындылай келе, мен Роберт Шилдттің әйгілі «Java: Жаңадан бастаушыға арналған нұсқаулық» кітабын ұсынамын - менің ойымша, онда ламбдалар мен функция сілтемелері өте ақылға қонымды сипатталған.
Пікірлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION