Sobes.tech
Middle+

Πώς λειτουργούν οι εκφράσεις lambda και τι συμβαίνει 'κάτω από το καπό';

sobes.tech AI

Απάντηση από AI

Οι εκφράσεις lambda στη Java λειτουργούν μέσω των ακόλουθων μηχανισμών:

  1. Λειτουργικά interfaces: Μια έκφραση lambda πρέπει να ανατεθεί σε μια μεταβλητή, να επιστραφεί από μια μέθοδο ή να περαστεί σε μια μέθοδο, του οποίου ο τύπος είναι ένα λειτουργικό interface. Ένα λειτουργικό interface είναι ένα interface με ακριβώς μία αφηρημένη μέθοδο (SAM - Single Abstract Method). Η διακόσμηση @FunctionalInterface είναι προαιρετική, αλλά συνιστάται για έλεγχο από τον μεταγλωττιστή.

  2. Δυναμική κλήση (invokeDynamic): Αντί να δημιουργεί μια ανώνυμη εσωτερική κλάση σε bytecode κατά τη μεταγλώττιση, όπως γινόταν σε παλαιότερες εκδόσεις ανώνυμων κλάσεων, η Java χρησιμοποιεί την εντολή invokeDynamic. Αυτή η εντολή προστέθηκε στη Java 7 για την υποστήριξη δυναμικών γλωσσών στην JVM και στη Java 8 χρησιμοποιείται για την υλοποίηση εκφράσεων lambda και αναφορών σε μεθόδους. Το invokeDynamic καθυστερεί την επίλυση της κλήσης μεθόδου μέχρι την εκτέλεση (runtime).

Τι συμβαίνει "υπό την κουκούλα":

Στο στάδιο της μεταγλώττισης:

  • Ο μεταγλωττιστής αναλύει την έκφραση lambda.
  • Προσδιορίζει τον τύπο του λειτουργικού interface στο οποίο αναφέρεται η lambda.
  • Δημιουργεί μια ειδική συνθετική μέθοδο (συνήθως με το πρόθεμα lambda$) στην ίδια κλάση, η οποία περιέχει τη λογική της έκφρασης lambda. Αυτή η μέθοδος έχει την ίδια υπογραφή με τη μέθοδο του αφηρημένου interface.
  • Στη θέση χρήσης της lambda, ο μεταγλωττιστής δημιουργεί την εντολή invokeDynamic. Αυτή η εντολή περιέχει μια αναφορά στη bootstrap method (LambdaMetafactory.metafactory).

Στο στάδιο της εκτέλεσης (runtime):

  • Όταν η JVM συναντά την εντολή invokeDynamic, καλεί για πρώτη φορά τη bootstrap method (LambdaMetafactory.metafactory).
  • Η bootstrap method (LambdaMetafactory.metafactory) δημιουργεί δυναμικά στη μνήμη το call site.
  • Το call site δημιουργεί και επιστρέφει ένα στιγμιότυπο μιας κλάσης που υλοποιεί το λειτουργικό interface. Αυτό το στιγμιότυπο μπορεί να είναι:
    • Δημιουργημένο επί τόπου (π.χ., με τη βοήθεια ενός Constant CallSite).
    • Caching για μελλοντική χρήση (για stateless lambda).
    • Αναπαριστάται ως αναφορά σε ήδη υπάρχον στιγμιότυπο του λειτουργικού interface (π.χ., για stateless lambda που υλοποιεί το Serializable).
  • Αυτή η γεννημένη κλάση διαβιβάζει την κλήση της αφηρημένης μεθόδου του λειτουργικού interface στη γεννημένη συνθετική μέθοδο (lambda$).
  • Μετά την πρώτη κλήση, το bootstrap method αποθηκεύει το αντικείμενο που πρέπει να επιστραφεί, και οι επόμενες κλήσεις invokeDynamic για την ίδια lambda μπορούν γρήγορα να αποκτήσουν αυτό το αντικείμενο ή να το καλέσουν απευθείας, χωρίς να επανακαλέσουν το bootstrap method.

Έτσι, οι lambda δεν δημιουργούν πάντα μια νέα κλάση για κάθε δημιουργία. Για lambda που δεν καταγράφουν μεταβλητές από το εξωτερικό πλαίσιο (stateless lambda), η υλοποίηση του λειτουργικού interface μπορεί να είναι singleton ή μια επαναχρησιμοποιήσιμη παρουσία. Οι lambda που καταγράφουν μεταβλητές (stateful lambda) απαιτούν τη δημιουργία νέας κλάσης που θα αποθηκεύει τις καταγεγραμμένες μεταβλητές.

Η χρήση του invokeDynamic επιτρέπει στον JIT-compiler να βελτιστοποιεί την εκτέλεση των κλήσεων μεθόδων του λειτουργικού interface πιο αποτελεσματικά από το παραδοσιακό μοντέλο ανώνυμων κλάσεων, καθώς ο JIT μπορεί να λαμβάνει πιο ενημερωμένες αποφάσεις κατά την εκτέλεση.

// Παράδειγμα λειτουργικού interface
@FunctionalInterface
interface MyFunction {
    String apply(String input);
}

public class LambdaExample {
    public static void main(String[] args) {
        // Χρήση lambda έκφρασης
        MyFunction func = s -> "Processed: " + s;

        String result = func.apply("Test");
        System.out.println(result);
    }
}