Skip to main content

ลองเล่น Lambda Expression ฟีเจอร์เด่นใน Java 8

ประวัติความเป็นมาของ Lambda expression

Lambda expression ไม่ใช่สิ่งแปลกใหม่ในวงการภาษาโปรแกรม (Programming Language) เพราะ lambda มันเป็นแกนหลักของการเขียนโปรแกรมเชิงฟังก์ชัน (Functional Programming) ซึ่งมีอายุมานานมากแล้ว แต่ Java เพิ่งนำเอาคุณสมบัตินี้เอามาใส่ลงในเวอร์ชัน 8

หากจะกล่าวถึงที่มาของ lambda คงต้องไปดูที่ถึงที่มาของ lambda calculus ซึ่งถูกสร้างขึ้นมาตั้งแต่ปี 1930 โดยนักคณิตศาสตร์ชาวอเมริกัน Alonzo Church เพื่อใช้ในการแก้โจทย์ปัญหาทางคณิตศาสตร์ที่มีความซับซ้อน ในบางครั้งสมการทางคณิตศาสตร์ที่ยาวไปอาจจะทำให้เกิดความซับซ้อนโดยใช่เหตุ lambda calculus จะทำการยุบบางส่วนของสมการนั้นออกมาเป็นฟังก์ชันย่อยๆ เพื่อทำให้สมการนั้นเข้าใจง่ายขึ้น

ต่อมาหลักการของ lambda calculus ได้ถูกนำไปใช้ใน Turing Machine ซึ่งเป็นแบบจำลองในอุดมคติของ Alan Turing ที่ต่อมากลายเป็นต้นแบบที่ถูกนำไปใช้ในการผลิต Von Neumann Machine ซึ่ง Von Neumann Machine ตัวนี้ได้กลายเป็นต้นแบบของคอมพิวเตอร์เครื่องแรกของโลกในเวลาต่อมา

ท้ายที่สุดแนวคิดของ lambda calculus ก็ถูกนำมาแปลงเป็นภาษาโปรแกรมที่เราเรียกว่า functional programming แต่ว่ากลับไม่ประสบความสำเร็จมากนัก จนกระทั่งเพิ่งมาได้รับความนิยมในช่วงทศวรรษปี 90 หรือที่เรารู้จักกันในภาษา Python หรือ Ruby เนื่องจากความร้อนแรงของ functional programming ทำให้หลายๆ ภาษาจึงได้มีการนำฟีเจอร์หลักอย่าง lambda expression ใส่เข้าไปในภาษาของตัวเอง อย่างเช่น Java

ทำไมต้องเอา lambda expression เข้ามาใน Java

เชื่อว่านักพัฒนาหลายๆ ท่านคงเคยเห็นหรือเคยใช้ interface เหล่านี้ java.lang.Runnable, java.awt.event.ActionListener, java.util.Comparator, java.util.concurrent.Callable ซึ่ง interface เหล่านี้ภายในนั้นจะมีแค่ abstract method เพียงหนึ่งอันเท่านั้น

นอกจาก interface ที่ยกมาในตัวอย่างข้างต้นแล้ว ยังมี interface แบบนี้อีกมากมายใน JDK หรือแม้กระทั่งใน lib opensource ต่างๆ ซึ่ง interface เหล่านี้เราจะเรียกว่า Single Abstract Method interface (SAM Interface)

ตัวอย่างของ SAM Interface หรือ Functional Interface ใน Java 8
package java.lang;

@FunctionalInterface
public interface Runnable {
  public abstract void run();
}

โดยปกติแล้วเวลาเราเรียกใช้ interface เหล่านี้ เราจะไม่สร้าง class ขึ้นมาเพื่อ implement interface เหล่านี้ แต่เราจะสร้างเป็น anonymous inner class ขึ้นมาแทนที่จะสร้าง class ขึ้นมาเพื่อ implement interface เหล่านี้ตรงๆ เช่น
public class Application {

    public static void main(String[] args){
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous inner class Runnable.");
            }
        };
        runnable.run();
    }
}

แต่พอใน Java 8 พวก SAM interface ทั้งหลายจะถูกเปลี่ยนชื่อใหม่เป็น Functional Interface (FI)แทน และการ implement interface แบบ anonymous class ก็จะถูกแทนที่โดย lambda expression ทำให้การสร้าง anonymous inner class เพื่อ implement FI นั้นสั้นลงจากหลายบรรทัดเหลือเพียงไม่กี่บรรทัด
public class Application {

    public static void main(String[] args){
        Runnable runnable = () -> System.out.println("Lambda expression Runnable");
        runnable.run();
    }
}

ส่วนเรื่องรายละเอียดของ Functional Interface เราจะมากล่าวกันใน blog ถัดไป (ถ้าไม่ขี้เกียจ)

เกริ่นนำ

lambda expression สำหรับใน Java ถูกนำมาใช้ในการสร้าง FI ในรูปแบบที่ง่ายและรวบรัดกว่าแบบเก่า นอกจากนี้ Java ยังนำเอาแนวคิดของ lambda expression มาปรับปรุงในส่วนของไลบารี่ Collection ที่ทำให้การดึง กรอง เลือก ข้อมูลใน Collection เป็นไปได้ง่ายขึ้น และยังรองรับการทำงานพร้อมกันและเพิ่มประสิทธิภาพในการประมวลผลแบบ multi-core

ใน Java 8 นอกจาก FI แบบเดิมๆ แล้ว เช่น java.lang.Runnable, java.awt.event.ActionListener, java.util.Comparator, java.util.concurrent.Callable แต่ยังมี FI อีกหลายๆ ตัวที่เพิ่มเข้ามา ซึ่ง FI เหล่านี้เป็นรูปแบบโครงร่างพื้นฐานที่มักจะมีอยู่ให้เห็นทั่วไปใน Java 8 บน package java.util.function

FI พวกนี้เราจะเห็นได้บ่อยๆ ในไลบารี่ Collection และ Stream ซึ่ง FI พวกนี้นี่เองที่ทำให้การดึง กรอง เลือกข้อมูลใน Collection เป็นไปได้โดยง่าย

Lambda Expression Syntax

Argument List
Arrow Token
Method Body
(int x, int y)
->
{x + y}

  • Argument List: เป็นส่วนที่เราต้องใส่ argument ตาม abstract method ที่ได้ประกาศไว้ใน functional interface แต่ถ้าไม่มี argument ให้ใช้ () แทน
  • Arrow Token: เป็นตัวขั้นกลางระหว่าง argument และเนื้อหาใน method
  • Method Body: เป็นส่วนของ coding ที่เราจะใส่เข้าไปในการ implement abstract method
  (int x, int y) -> {return x + y;}
เป็นการ implement method ที่มี parameter สองตัว โดยในตัว method จะมีการทำงานเอาค่า x และ y มาบวกกันแล้วนำผลลัพธ์นั้น return ค่ากลับไป
  x, y -> x + y
เป็นการลดรูปจากตัวอย่างแรก เนื่องจากว่า functional interface จะมีเพียง abstract method เพียงตัวเดียวอยู่แล้ว ดังนั้นเราไม่จำเป็นที่จะต้องประกาศชนิดตัวแปรของ parameter ก็ได้ แต่การใส่ไว้จะช่วยทำให้การอ่าน code ได้ง่ายขึ้น

นอกจากนี้ ถ้าหากมีเพียง 1 statement เราสามารถย่อได้อีกโดยการนำเอา { และ } ออกได้ เช่นเดียวกับ keyword return ก็สามารถเอาออกได้เช่นกัน ตัว lambda expression จะรู้โดยตัวเองว่าต้องมีค่าผลลัพธ์ของ x + y คืนค่ากลับไป
  () -> 42
ในกรณีที่ไม่มี argument ให้ส่ง ให้เราใช้ () แทน

ลองใช้ lambda expression
interface Name{
    public void sayName(String name);
}

public class LambdaExample {
    public static void main(String args[]){
        Name name = new Name() {
            @Override
            public void sayName(String name) {
                System.out.println("My Name is " + name);
            }
        };
        myName(name, "John");
    }

    private static void myName(Name nameInstance, String name) {
        nameInstance.sayName(name);
    }
}
จากตัวอย่างด้านบนเป็นวิธีการสร้าง FI แบบเก่าที่ดูยาวและรกรุงรัง เรามาลองดูในรูปแบบ lambda expression
interface Name{
    public void sayName(String name);
}

public class LambdaExample {
    public static void main(String args[]){
        Name name = (String n) -> {System.out.println("My name is " + n);};
        myName(name, "John");
    }

    private static void myName(Name nameInstance, String name) {
        nameInstance.sayName(name);
    }
}
จริงๆ เราจะสามารถย่อได้สั้นกว่านี้ แต่การอ่านจะยากกว่าเดิม (ตามด้านล่าง) แต่การเขียนแบบด้านบนจะอ่านได้ง่ายกว่า
public static void main(String args[]){
    ...
    myName(n -> System.out.println("My name is " + n), "John");
    ...
}

Functional Interface หลักๆ ที่ควรรู้จัก

อย่างที่กล่าวไว้ข้างต้น ใน Java 8 มี FI พื้นฐานอยู่ใน package java.util.function ซึ่งในนั้นมี FI อยู่หลายๆ ตัว แต่ที่ควรรู้จักหลักๆ มีอยู่ 4 ตัว Predicate, Consumer, Supplier และ Function เราจะมาลองเจาะลึกกันในแต่ละตัว

Consumer: เป็น FI พื้นฐานที่พบเห็นบ่อยใน Collection อย่างใน method forEach โดยตัว Consumer จะรับ argument เพียงหนึ่งตัวและนำเอา argument นั้นมากระทำบางอย่าง แต่จะไม่มีการ return ค่าใดๆ กลับมา
package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Consumer<T> {
    ...
    void accept(T t);
    ...
}
ตัวอย่าง Consumer ใน Collection
import java.util.ArrayList;
import java.util.stream.Consumer;
import java.util.stream.Stream;

public class LambdaExample {
    public static void main (String args[]){
        ArrayList<Integer> integerList = new ArrayList<>();
        for (int i=0; i<10; i++) {
            integerList.add((int)(Math.random()*1000));
        }
        System.out.println("List All data");
        Consumer<Integer> printConsumer = (Integer i) -> System.out.print(i + ", ");
        integerList.forEach(printConsumer);
        //integerList.forEach(i -> System.out.print(i + ", "));
    }
}
การสร้าง FI สามารถย่อได้เหลือบรรทัดเดียวเหมือนใน comment ด้านล่าง แต่ที่สร้างแยกออกมาเพื่อให้เห็นที่มาที่ไปก่อนที่จะมาเห็นในรูปแบบที่ย่อ

ตัวอย่างจะแสดงให้เห็นถึงหลักการสร้าง FI โดย FI ที่เราสร้างต้องอ้างอิงกับ Generic class ของเป้าหมาย (ArrayList<Integer>) อย่าง Consumer ที่เราจะสร้างต้องใช้ Generic class เป็น Integer (Consumer<Integer>) เพราะเราจะเอาไปใช้กับ ArrayList<Integer>

Supplier: เป็น FI พื้นฐานที่ไม่ค่อยจะได้เห็นกันสักเท่าไร ทำหน้าที่ผลิตผลลัพธ์อะไรบางอย่างแล้วคืนค่ากลับไป โดยผลลัพธ์ที่ได้มาในแต่ละครั้งอาจจะเหมือนเดิมหรือไม่เหมือนเดิมทุกครั้ง ตามแต่ที่นักพัฒนาจะกำหนด
package java.util.function;

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
ตัวอย่างนี้จะสร้าง Supplier เพื่อสุ่มตัวเลขขึ้นเพื่อใส่เข้าไปใน ArrayList ของ Integer
import java.util.ArrayList;
import java.util.function.Supplier;

public class LambdaExample {
    public static void main (String args[]){
        Supplier<Integer> integerSupplier = () -> (int)(Math.random()*1000);
        ArrayList<Integer> integerList = new ArrayList<>();
        for (int i=0; i<10; i++) {
            integerList.add(integerSupplier.get());
        }
        System.out.println("List All data " + integerList);
    }
}
Supplier ที่เราสร้างขึ้นมาในบรรทัดที่ 6 จะทำหน้าที่สุ่มตัวเลขที่มีค่าระหว่าง 0-999 แล้วคืนค่ากลับไป โดยค่าดังกล่าวเราจะเอาไปใส่ลงไปใน ArrayList<Integer> (บรรทัดที่ 9)

Predicate: เป็น FI ที่เกี่ยวกับเงื่อนไข เมื่อมีการตรวจสอบเสร็จก็จะ return ค่า true หรือ false กลับมา เราจะพบบ่อยใน Stream API เพื่อใช้ในการคัดกรองข้อมูลเช่น filter()
package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Predicate<T> {
    ...
    boolean test(T t);
    ...
}
Predicate เป็นการสร้างเงื่อนไขแบบพลวัต (dynamic) ทำให้นักพัฒนาสามารถที่จะส่งเงื่อนไขแบบไม่ผูกมัดใดๆ เหมาะกับเงื่อนไขที่เปลี่ยนไปได้ตลอดเช่น method filter ใน Stream API ที่เราสามารถส่งเงื่อนไขเพื่อกรองข้อมูลตามโครงสร้างข้อมูลของผู้ใช้
import java.util.ArrayList;
import java.util.stream.Stream;

public class LambdaExample {
    public static void main (String args[]){
        Supplier<Integer> integerSupplier = () -> (int)(Math.random()*1000);
        ArrayList<Integer> integerList = new ArrayList<>();
        for (int i=0; i<10; i++) {
            integerList.add(integerSupplier.get());
        }
        System.out.println("List All data " + integerList);
        Stream<Integer> stream = integerList.stream();
        Stream<Integer> filterStream = stream.filter((Integer i) -> i < 200);
        System.out.println("List All data < 200");
        stream.forEach(i -> System.out.print(i + ", "));
    }
}
ตรงบรรทัดที่ 13 เป็นสร้าง Predicate ที่มีเงื่อนไขว่า ค่าที่จถูกกรองออกไปเป็นค่าที่น้อยกว่า 200 ลงไป

Function: เป็น FI ที่รวมเอาความสมารถของ Supplier และ Consumer รวมเข้าไว้ด้วยกัน คือสามารถรับ argument เข้ามาแล้วประมวลผลและคืนค่ากลับไป (return) เหมือนเป็นฟังก์ชันจริงๆ ตามชื่อของมัน
package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Function<T, R> {
    ...
    R apply(T t);
    ...
}
ตัวอย่างการคิดเกรดแบบอิงเกณฑ์และอิงกลุ่ม
import java.util.ArrayList;
import java.util.IntSummaryStatistics;
import java.util.function.Function;
import java.util.function.Supplier;

public class LambdaExample {
    public static void main (String args[]){
        Supplier<Integer> integerSupplier = () -> (int)(Math.random()*101);
        ArrayList<Integer> scoreList = new ArrayList<>();
        for (int i=0; i<10; i++) {
            scoreList.add(integerSupplier.get());
        }
        System.out.println("List All score " + scoreList);
        ArrayList<String> criteriaGradeList = calculateGrade(criteriaReference(), scoreList);
        System.out.println("List All grading by Criteria Reference" + criteriaGradeList);
        ArrayList<String> normGradeList = calculateGrade(normReferenced(), scoreList);
        System.out.println("List All grading by Norm Reference" + normGradeList);
    }

    private static ArrayList<String> calculateGrade(Function<ArrayList<Integer>, ArrayList<String>> calculateFunction,
                                                    ArrayList<Integer> scoreList){
        return calculateFunction.apply(scoreList);
    }

    private static Function<ArrayList<Integer>, ArrayList<String>> criteriaReference(){
        return input -> {
            ArrayList<String> gradeList = new ArrayList<>();
            for (Integer score : input) {
                if(score >= 80){
                    gradeList.add(score + " = A");
                }else if(score >= 70){
                    gradeList.add(score + " = B");
                }else if(score >= 60){
                    gradeList.add(score + " = C");
                }else if(score >= 50){
                    gradeList.add(score + " = D");
                }else{
                    gradeList.add(score + " = F");
                }
            }
            return gradeList;
        };
    }

    private static Function<ArrayList<Integer>, ArrayList<String>> normReferenced(){
        return input -> {
            ArrayList<String> gradeList = new ArrayList<>();
            IntSummaryStatistics statistics = input.stream().mapToInt(x -> x).summaryStatistics();
            double average = statistics.getAverage();
            double sumSquareDifference = 0;
            for (Integer score : input) {
                sumSquareDifference += Math.pow(score - average, 2);
            }
            double variance = sumSquareDifference / input.size();
            double sd = Math.sqrt(variance);
            double criteria = sd/4;
            double gradeA = average + criteria * 2;
            double gradeB = average + criteria;
            double gradeC = average;
            double gradeD = average - criteria;
            System.out.printf("Norm Criteria: A>=%f, B>=%f, C>=%f, D>=%f\n", gradeA, gradeB, gradeC, gradeD);
            for (Integer score : input) {
                if(score >= gradeA){
                    gradeList.add(score + " = A");
                }else if(score >= gradeB){
                    gradeList.add(score + " = B");
                }else if(score >= gradeC){
                    gradeList.add(score + " = C");
                }else if(score >= gradeD){
                    gradeList.add(score + " = D");
                }else{
                    gradeList.add(score + " = F");
                }
            }
            return gradeList;
        };
    }
}
เมื่อมีการนำ FI อย่าง Function มาใช้จะเห็นได้ว่าการเขียน code ของเรายืดหยุ่นขึ้น ในอนาคตถ้ามีการเพิ่มวิธีการคิดเกรดแบบใหม่ขึ้น เราแทบไม่ต้องไปแก้ method สำหรับการคิดเกรดเลย (calculateGrade()) เราแค่ส่ง Function ใหม่ที่เราเขียนการจัดการเกรดแบบใหม่เข้าไปเท่านั้น

ถ้าหากเป็น Java ในเวอร์ชันเก่าเราจะใช้ Template Method เข้ามาช่วยแก้ปัญหานี้ซึ่งจะยุ่งยากกว่าการที่เรานำ FI มาช่วย

และนี่คือตัวอย่างและวิธีการใช้งาน lambda expression แต่ไม่ใช่ว่า lambda expression จะใช้ได้กับ Interface หรือ Abstract class ทั่วไปได้ จะใช้ได้กลับ Interface หรือ Abstract class ที่มี Abstract method ที่ยังไม่ implement เพียงหนึ่ง Method เท่านั้น หรือ interface ที่มีการระบุเจาะจงว่าเป็น FI

นอกเหนือจาก FI พื้นฐานใน package java.util.function แล้ว เรายังสามารถสร้าง FI ขึ้นมาเองได้ เพื่อที่จะรองรับการทำงานของผู้ใช้ที่นอกเหนือจาก FI พื้นฐานที่ระบบเตรียมมาให้ ดังนั้นเรื่องต่อไปที่ควรรู้ต่อจาก lambda expression นั่นก็คือเรื่องของ Functional Interface (FI) นั่นเอง เพราะสองสิ่งนี้แยกกันไม่ได้

เรียบเรียงจาก Lambda Quick Start by Oracle

Comments

Popular posts from this blog

ลองเล่นและเรียนรู้พื้นฐานขั้นต้นของ Spring Framework

** สำหรับใครที่ไม่เคยเรียนรู้ในด้านของ Java EE หรือ J2EE อาจจะมึนงงกับศัพท์หน่อยครับ ทำไมต้อง Spring Spring เป็น framework ที่นิยมมากในการนำไปสร้างระบบในระดับ enterprise ในเริ่มแรกที่ Spring เกิดมา มีจุดมุ่งหมายเพื่อที่จะมาแทนที่มาตรฐานของ Java อย่าง J2EE (Java 2 Enterprise Edition) ที่มันทั้งหน่วงทั้งอืดและยุ่งยาก โดยเฉพาะในส่วนของ EJB (Enterprise Java Bean) ที่ถือว่าเป็นฝันร้ายของนักพัฒนา ทำให้กูรูสาย Java ในช่วงนั้นถึงกับแนะนำว่า ถ้าจำเป็นที่ต้องพัฒนาระบบด้วย J2EE จงอย่าใช้ EJB ถึงขั้นถึงกับมีหนังสือแนะแนวทางการพัฒนาระบบ J2EE โดยไม่ใช้ EJB อย่างไรก็ตามทาง Sun ผู้เป็นเจ้าของ Java ในสมัยนั้น ถึงกับต้องมาล้างระบบ J2EE ใหม่ในปี 2006 จัดการใน EJB ให้ใช้ง่ายขึ้น มีประสิทธิภาพมากขึ้น และมีการเปลี่ยนชื่อจาก J2EE เป็น Java EE (Java Enterprise Edition) เพื่อลบภาพอันเลวร้ายของเดิมให้หมด และได้มีการนำฟีเจอร์เด็ดๆ ของ open source framework หลายๆ ตัว อย่างเช่นแกนหลักของ Spring อย่าง IoC (Inversion of Control) หรือ OR Mapping (Object Relational Mapping) ที่เป็นที่นิยมอย่าง Hibernate แต่ก็ไ

Inversion of Control และ Dependency Injection

Inversion of Control (IoC) คืออะไร IoC เป็นทฤษฏีที่ว่าด้วย การลดความผูกมัด (dependency) กันในระหว่าง module เพื่อให้ application ของเราแก้ไข (maintain) ต่อเติม (extensible) หรือทดสอบ (test) ได้ง่ายขึ้น ซึ่งเอาจริงๆ IoC เป็นอะไรที่ทำให้เราสับสนและงุนงงมากๆ ว่ามันคืออะไร หลายๆ คนจึงยกให้ว่า IoC คือ Dependency Injection (DI) ซึ่งจริงๆ มันก็ไม่ถูกซะทีเดียว  Dependency คืออะไร Dependency คือการผูกมัดที่เกิดขึ้นในระบบ เมื่อ module นึงมีการเรียกใช้อีก module นึงด้วยการอ้างอิง (reference) ตรงๆ แล้วอะไรที่เรียกว่าการ อ้างอิง (Reference) แบบตรงๆ   อย่างภาพ diagram ด้านบน class LogEngine มีการเรียกใช้ ConsoleLog โดยตรง ซึ่งมองผ่าน diagram อาจจะไม่เห็นภาพลองดู code กัน public class ConsoleLog { public void openLog(){ //do something to open log } public void log(String message){ //do something to log } public void closeLog(){ //do something to close log } } public class LogEngine { private ConsoleLog log; public LogEngine(){