Skip to content

4. تعبيرات لامدا في Java 8

4.1. المثال-01 - الواجهات الوظيفية ولامبدا

  

انظر إلى الكود التالي:


package dvp.java8.lambdas;
 
public class Exemple01 {
  public static void main(String[] args) {
 
    // anonymous classes
    I1 ia1 = new I1() {
      @Override
      public void doSomething() {
        System.out.println("ia1.doSomething");
      }
    };
 
    I2 ia2 = new I2() {
      @Override
      public String getSomething(double value) {
        return String.format("ia2.getSomething(%s)", value);
      }
    };
 
    // lambdas
    I1 ib1 = () -> System.out.println("ib1.lambda");
    I2 ib2 = (value) -> String.format("ib2.lambda(%s)", value);
    I1 ib3 = () -> {
      System.out.println("ib3.lambda");
    };
    I2 ib4 = (double value) -> {
      return String.format("ib4.lambda(%s)", value);
    };
 
    // app
    ia1.doSomething();
    System.out.println(ia2.getSomething(4.3));
    ib1.doSomething();
    System.out.println(ib2.getSomething(5.8));
    ib3.doSomething();
    System.out.println(ib4.getSomething(10.1));
  }
}
 
@FunctionalInterface
interface I1 {
  void doSomething();
}
 
@FunctionalInterface
interface I2 {
  String getSomething(double value);
}
  • الأسطر 41–44: تعريف واجهة وظيفية I1. الواجهة الوظيفية هي واجهة تحتوي على طريقة واحدة فقط. وهي لا تعتمد على وجود التعليق التوضيحي [@FunctionalInterface] في السطر 41، وهو اختياري؛
  • الأسطر 46–49: واجهة وظيفية ثانية I2؛
  • الأسطر 6–19: يتم تنفيذ الواجهتين I1 و I2 باستخدام فئات مجهولة، وهو الحل الأكثر شيوعًا قبل إدخال دوال لامدا؛
  • الأسطر 21–29: يتم تنفيذ الواجهتين I1 و I2 باستخدام دوال لامدا؛
  • الأسطر 7–12: تنفيذ الواجهة I1 باستخدام فئة مجهولة. صيغة تنفيذ واجهة I باستخدام فئة مجهولة هي كما يلي:
I i=new I(){
    @Override
    public T1 m1(...){
...
    }
    public T2 m2(...){
...
    }
}

حيث m1، m2، ... هي الطرق التي حددتها الواجهة I.

  • الأسطر 14–19: تنفيذ الواجهة I2 باستخدام فئة مجهولة؛
  • السطر 22: تنفيذ الواجهة I1 باستخدام دالة لامدا. هنا، نستفيد من حقيقة أن الواجهة الوظيفية تحتوي على طريقة واحدة فقط. ثم تقوم دالة لامدا بتنفيذ هذه الطريقة الوحيدة M. وصيغتها كما يلي:
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

يمكن حذف الأنواع T1 و T2 و Tn إذا كان بإمكان المُترجم استنتاجها من السياق (استنتاج النوع).

  • السطر 22: تنفيذ الطريقة [I1.doSomething] بالتوقيع:

void doSomething();

[doSomething] هي طريقة لا تحتوي على معلمات ولا تُرجع أي نتيجة. يمكن كتابة تنفيذها باستخدام لامدا كما في السطر 22 أو كما في الأسطر 24–26، أي يمكن وضع أقواس معقوفة حول كود دالة لامدا. إذا كان هذا الكود يحتوي على جملة واحدة فقط، كما هو الحال هنا، فيمكن حذف هذه الأقواس المعقوفة؛

  • السطر 23: تنفيذ طريقة [I1.getSomething] بالتوقيع التالي:

String getSomething(double value);

تأخذ [getSomething] معلمة من النوع [double] وتُرجع نتيجة من النوع [String]. يمكن أن يكون تنفيذ لامدا الخاص بها هو الموجود في السطر 23 أو الموجود في الأسطر 27–29. في التنفيذ الموجود في السطر 23:

  • يتم حذف نوع المعلمة [value]. وسيتم عندئذٍ استخدام النوع [double] الموجود في توقيع [getSomething
  • لا يتم وضع كود لامدا بين قوسين. تكون نتيجة لامدا عندئذ هي قيمة التعبير الوحيد في ذلك الكود، وهنا: String.format("ib2.lambda(%s)", value);

في التنفيذ في الأسطر 27-29:

  • يتم الإعلان صراحةً عن نوع المعلمة [value
  • نستخدم [return] لإرجاع نتيجة لامدا. في هذه الحالة، يجب استخدام الأقواس المتعرجة؛
  • الأسطر 32–37: نستدعي الدوال المجهولة المختلفة ولامبدا؛

والنتيجة التي تم الحصول عليها هي كما يلي:

1
2
3
4
5
6
ia1.doSomething
ia2.getSomething(4.3)
ib1.lambda
ib2.lambda(5.8)
ib3.lambda
ib4.lambda(10.1)

4.2. مثال-02 - الواجهة الوظيفية Predicate<T>

  

في معظم الأحيان، نتعامل مع واجهات وظيفية من المكتبات بدلاً من الواجهات الوظيفية التي نحددها بأنفسنا. هنا، نحن مهتمون بالواجهة الوظيفية [Predicate] المحددة في حزمة [java.util.function]، التي تحتوي على معظم الواجهات الوظيفية في Java 8. وهي محددة على النحو التالي:

Image

ذكرنا أن الواجهة الوظيفية تحتوي على طريقة واحدة فقط. ولكن هنا، هناك أربع طرق. كان أحد الابتكارات الأخرى التي أدخلتها Java 8 هو مفهوم الطريقة الافتراضية في الواجهة، والتي يتم تمييزها بالكلمة الرئيسية [default]. لدينا هنا ثلاث طرق من هذا النوع. تتميز هذه الطرق بوجود تنفيذ افتراضي لها. وبالتالي، لا يوجد أي شرط يقتضي من الفئة التي تنفذ واجهة ذات طرق افتراضية أن تنفذها. وبالتالي، فإن الفئة التي ترغب في تنفيذ واجهة [Predicate] لديها طريقة واحدة فقط يجب أن تنفذها: طريقة [test]. وبالتالي، فإن واجهة [Predicate] هي بالفعل واجهة وظيفية. وبالتالي، يمكننا القول إن الواجهة الوظيفية هي واجهة يحتوي تنفيذها على طريقة إلزامية واحدة فقط. وإذا كانت تحتوي على أكثر من طريقة واحدة، فيجب أن تحتوي الطرق الأخرى على الكلمة الرئيسية [default].

تأخذ الطريقة [Predicate<T>.test] معلمة من النوع T وتُرجع قيمة منطقية. تُستخدم هذه الواجهة عمومًا لتصفية المجموعات. لتوضيح استخدامها، سنستخدم البيانات التالية:


package dvp.data;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Personne {
 
  public enum Sexe {HOMME,FEMME};
 
  // data
  private String nom;
  private int age;
  private double poids;
  private Sexe sexe;

  // manufacturers
  public Personne() {
 
  }
 
  public Personne(String nom, Sexe sexe, int age, double poids) {
    this.nom = nom;
    this.sexe=sexe;
    this.age = age;
    this.poids = poids;
  }
 
  // getters and setters
...
 
  // toString
  @Override
  public String toString(){
    try {
      return new ObjectMapper().writeValueAsString(this);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • الأسطر 32–38: تُرجع طريقة [toString] سلسلة JSON الخاصة بالشخص؛

تُعرّف فئة [People] قائمة تضم 3 أشخاص:


package dvp.data;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import java.util.Arrays;
import java.util.List;
 
public class Personnes {
  private static List<Personne> personnes = Arrays.asList(new Personne("jean", Personne.Sexe.HOMME, 20, 70),
    new Personne("marie", Personne.Sexe.FEMME, 10, 30), new Personne("camille", Personne.Sexe.FEMME, 30, 55));
 
  public static List<Personne> get() {
    return personnes;
  }
 
  public static String toString(List<Personne> liste) {
    try {
      return new ObjectMapper().writeValueAsString(liste);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • السطران 10–11: قائمة تضم 3 أشخاص؛
  • الأسطر 13–15: طريقة ثابتة لاسترداد هذه القائمة؛
  • الأسطر 17–23: طريقة ثابتة للحصول على سلسلة JSON لقائمة الأشخاص التي تم تمريرها كمعلمة؛

سيتم استخدام هذه البيانات بواسطة الكود التالي:


package dvp.java8.lambdas;
 
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple02 {
  public static void main(String[] args) {
    // predicate implemented by anonymous class
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
// predicate implemented by a lambda
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // list of persons
    List<Personne> personnes = Personnes.get();
    // displays
    System.out.println(Personnes.toString(filterPersonnes(personnes, filterAge)));
    System.out.println(Personnes.toString(filterPersonnes(personnes, filterPoids)));
  }
 
  private static List<Personne> filterPersonnes(List<Personne> personnes, Predicate<Personne> filter) {
      // [filter] filters the [people] list
    List<Personne> personnesFiltrées = new ArrayList<>();
    for (Personne p : personnes) {
      if (filter.test(p)) {
        personnesFiltrées.add(p);
      }
    }
    return personnesFiltrées;  }
}
  • الأسطر 13–18: تنفيذ واجهة Predicate<Person> باستخدام فئة مجهولة. هذا مرشح يعتمد على وزن الشخص؛
  • السطر 20: تنفيذ واجهة Predicate<Person> باستخدام دالة لامدا. هذا مرشح يعتمد على عمر الشخص. بناءً على ما قيل، كان من الممكن أيضًا كتابته على النحو التالي:

        Predicate<Personne> filterAge = (Personne p) -> {
            return (p.getAge() < 28);
        };

لكن النسخة الموجودة في السطر 20 أكثر إيجازًا. يتم استنتاج نوع المعلمة p من السياق. هنا، نقوم بإنشاء نوع [Predicate<Person>]. ثم يكون للطريقة المنفذة التوقيع [boolean test(Person param)]. لذلك، فإن النوع الضمني لـ p في السطر 20 هو النوع [Person

  • السطر 22: نسترد قائمة محددة مسبقًا بالأشخاص؛
  • السطر 24: نقوم بتصفيتها حسب العمر؛
  • السطر 25: نقوم بتصفيتها حسب الوزن. في كلتا الحالتين، نعرض سلسلة JSON للقائمة التي تمت تصفيتها؛
  • الأسطر 28–37: طريقة ثابتة
    • تأخذ كمعلمات: قائمة بالأشخاص المراد تصفيتهم والمعيار. المعيار هو مثيل لواجهة [Predicate<Person>]. للسهولة، نشير هنا وفي أماكن أخرى من الوثيقة إلى مثيل واجهة I كمثيل لفئة C التي تنفذ I؛
    • تُرجع القائمة المُصفاة كنتيجة؛
  • السطر 32: نستخدم طريقة [test] لواجهة [Predicate]. اعتمادًا على عامل التصفية الذي تم تمريره إلى الطريقة، ستكون طريقة [test]:

return personne.getPoids()<50;

أو


return p.getAge() < 28

يؤدي تنفيذ فئة [Exemple02] إلى النتيجة التالية:

[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]
[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]

4.3. مثال-03 - واجهة الوظيفة Function<T,R>

  

يتم تعريف الواجهة الوظيفية Function<T,R> على النحو التالي:

Image

تحتوي الطريقة الوحيدة للواجهة على التوقيع R apply(T t). وتُستخدم عمومًا لإنشاء نوع Collection<R> جديد من نوع Collection<T>. لتوضيح هذه الواجهة، سنستخدم الكود التالي:


package dvp.java8.lambdas;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.data.Personne;
import dvp.data.Personnes;
 
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
 
public class Exemple03 {
  public static void main(String[] args) throws JsonProcessingException {
    // implementation with anonymous class
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // implementation with lambda
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // list of persons
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // displays
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }
 
  // transformation List<Person> --> List<T>
  private static <T> List<T> mapPersonnes(List<Personne> personnes, Function<Personne, T> mapper) {
    List<T> maps = new ArrayList<>();
    for (Personne p : personnes) {
      maps.add(mapper.apply(p));
    }
    return maps;
  }
}
  • الأسطر 15–20: ننفذ الواجهة [Function<Person, String>] باستخدام فئة مجهولة تقوم بتحويل Person → String؛
  • السطر 22: نقوم بتنفيذ الواجهة [Function<Person, Integer>] باستخدام دالة لامدا تقوم بتحويل Person إلى Integer؛
  • الأسطر 33–39: طريقة ثابتة
    • تأخذ معلمتين: الأولى، من النوع List<Personوهي قائمة بالأشخاص المراد تحويلهم. والثانية، من النوع Function<Person, Tوهي دالة تأخذ كل شخص من القائمة وتُنشئ كائنًا من النوع T؛
    • تُرجع قائمة من نوع List<T> حيث ينتج كل عنصر عن تحويل Person -> T؛
  • الأسطر 35–37: يتم تطبيق تحويل Person -> T. إذا كانت المعلمة الثانية للطريقة هي كائن [mapToName]، يتم إجراء تحويل Person -> String. إذا كانت كائن [mapToAge]، يتم إجراء تحويل Person -> Integer؛

والنتيجة هي كما يلي:

["jean","marie","camille"]
[20,10,30]

4.4. مثال-04 - الواجهة الوظيفية Consumer<T>

  

يتم تعريف الواجهة الوظيفية Consumer<T> على النحو التالي:

Image

الطريقة الوحيدة للواجهة لها التوقيع التالي: void accept(T t). تعالج هذه الطريقة (تستهلك) معلمتها ولا تُرجع أي نتيجة. لتوضيح ذلك، سنستخدم الكود التالي:


package dvp.java8.lambdas;
 
import java.util.List;
import java.util.function.Consumer;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple04 {
    public static void main(String[] args) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // anonymous implementation
        Consumer<Personne> consumerAge = new Consumer<Personne>() {
            @Override
            public void accept(Personne personne) {
                System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
            }
        };
        // immplémentaton lambda
        Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());        
        // displays
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • الأسطر 14–19: يتم تنفيذ الواجهة [Consumer<Person>] بواسطة فئة مجهولة تعرض طريقة [accept] الخاصة بها اسم الشخص وعمره؛
  • السطر 21: يتم تنفيذ الواجهة [Consumer<Person>] بواسطة دالة لامدا تُظهر طريقة [accept] الضمنية فيها اسم الشخص ووزنه؛
  • الأسطر 23–25: يتم استهلاك قائمة الأشخاص بواسطة التنفيذ [consumerAge
  • الأسطر 27–29: يتم استهلاك قائمة الأشخاص بواسطة التنفيذ [consumerWeight

والنتائج هي كما يلي:

1
2
3
4
5
6
7
 age de jean = 20
 age de marie = 10
 age de camille = 30
--------
 poids de jean = 70.0
 poids de marie = 30.0
poids de camille = 55.0

4.5. مثال-05 - الواجهة الوظيفية BiConsumer<T,U>

  

يتم تعريف الواجهة الوظيفية BiConsumer<T,U> على النحو التالي:

Image

الطريقة الوحيدة للواجهة لها التوقيع التالي: void accept(T t, U u). وهي تستهلك النوع T مع المعلومات الإضافية U u. سنوضح استخدامها بالرمز التالي:


package dvp.java8.lambdas;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
import java.util.List;
import java.util.function.BiConsumer;
 
public class Exemple05 {
  public static void main(String[] args) {
    // list of persons
    List<Personne> personnes = Personnes.get();
    // anonymous implementation
    BiConsumer<Personne, Integer> biconsumerAge = new BiConsumer<Personne, Integer>() {
      @Override
      public void accept(Personne personne, Integer integer) {
        personne.setAge(personne.getAge() + integer);
        System.out.printf("age de %s = %s%n", personne.getNom(), personne.getAge());
      }
    };
    // lambda implementation
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // displays
    for (Personne p : personnes) {
      biconsumerAge.accept(p, 100);
    }
    System.out.println("--------");
    for (Personne p : personnes) {
      biconsumerPoids.accept(p, 200);
    }
  }
}
  • الأسطر 14–20: تنفيذ واجهة BiConsumer<T,U> باستخدام فئة مجهولة. تستخدم طريقة [apply] المعلمة الثانية لتحديث عمر الشخص الذي تم تمريره كمعلمة أولى. ثم تعرض النتيجة؛
  • الأسطر 22–25: تنفيذ واجهة BiConsumer<T,U> باستخدام دالة لامدا. تستخدم الطريقة الضمنية [apply] المعلمة الثانية لتحديث وزن الشخص الذي تم تمريره كمعلمة أولى. ثم تعرض النتيجة؛
  • الأسطر 27-29: يتم استهلاك قائمة الأشخاص باستخدام تنفيذ [biconsumerAge
  • الأسطر 31–33: يتم استهلاك قائمة الأشخاص باستخدام تنفيذ [biconsumerWeight

النتائج التي تم الحصول عليها هي كما يلي:

1
2
3
4
5
6
7
age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0

4.6. مثال-06 - واجهة وظيفية BiFunction<T,U,R>

  

يتم تعريف الواجهة الوظيفية BiFunction<T,U,R> على النحو التالي:

Image

الطريقة الوحيدة للواجهة لها التوقيع: R apply(T t, U u). هذه الطريقة مشابهة لطريقة [BiConsumer.apply]، ولكن في حين أن الأخيرة لا تُرجع نتيجة، فإن طريقة [BiFunction.apply] تُرجع نتيجة. سنوضح استخدامها بالكود التالي:


package dvp.java8.lambdas;
 
import java.util.List;
import java.util.function.BiFunction;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple06 {
    public static void main(String[] args) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // anonymous implementation
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // lambda implementation
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // displays
        for (Personne p : personnes) {
            System.out.printf("age de %s = %s%n", p.getNom(), biFunctionAge.apply(p, 100));
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            System.out.printf("poids de %s = %s%n", p.getNom(), biFunctionPoids.apply(p, 200));
        }
    }
}
  • الأسطر 14–19: يتم تنفيذ واجهة BiFunction<Person, Integer, Integer> باستخدام فئة مجهولة. تُرجع طريقة [apply] الخاصة بها عمر الشخص الذي تم تمريره كمعلمة أولى، مضافًا إليه قيمة المعلمة الثانية؛
  • الأسطر 21–23: يتم تنفيذ واجهة BiFunction<Person, Integer, Double> باستخدام دالة لامدا. تُرجع الطريقة [apply] وزن الشخص الذي تم تمريره كمعلمة أولى مضافًا إليه قيمة المعلمة الثانية؛
  • الأسطر 25–27: يتم تطبيق تنفيذ [biFunctionAge] على الأشخاص؛
  • الأسطر 29–31: يتم تطبيق تنفيذ [biFunctionWeight] على الأشخاص؛

النتائج التي تم الحصول عليها هي كما يلي:

age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0

بالإضافة إلى دوال لامدا، أدخلت Java 8 نوع Stream<T>، الذي يمثل دفقًا من العناصر من النوع T. يمكن أن تخضع هذه العناصر لتحويلات متتالية تنفذها دوال لامدا. عندما يكون ذلك ممكنًا، وعندما يكون هناك معالجات متعددة، يمكن أحيانًا تنفيذ هذه التحويلات بالتوازي.

4.7. مثال-07 - الواجهة الوظيفية Supplier<T>

  

يتم تعريف الواجهة الوظيفية Supplier<T> على النحو التالي:

Image

الطريقة الوحيدة للواجهة لها التوقيع: T get()، ودورها هو إرجاع كائن من النوع T.

سنوضح هذه الواجهة الوظيفية باستخدام الكود التالي:


package dvp.java8.lambdas;
 
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple07 {
    public static void main(String[] args) {
        // anonymous implementation
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // list of persons
            List<Personne> personnes = Personnes.get();
 
            // interface implementation
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // operation
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }
 
    // person display
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • الأسطر 13–28: تنفيذ نوع Supplier<Person>؛
  • الأسطر 31–33: تتوقع الطريقة الثابتة [display] معلمة من النوع Supplier<Person>؛
  • الأسطر 25–27: استخدام مثيل Supplier<Person>؛

تم الحصول على النتائج التالية:

1
2
3
4
5
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}