[LG CNS AM Inspire CAMP 1기] Java (3) - 함수형 프로그래밍(람다식)

니니지·2025년 1월 14일

LG CNS AM Inspire Camp 1기

목록 보기
22/47
post-thumbnail

INTRO

함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다. 함수형 프로그래밍은 함수의 응용을 강조한다. 다수의 함수형 프로그래밍 언어들은 람다 계산을 발전시킨 것으로 볼 수 있다.

1. 함수형 프로그래밍

프로그램을 함수들의 조합으로 만드는 방식.
데이터나 상태를 변경하지 않고 새로운 값을 반환.
(동일한 입력값을 넣으면 동일한 결과를 반환 → 순수 함수(pure function))

단점 : 가독성
장점 : 코드의 재사용성

vs. 선언형 프로그래밍

⇒ 무엇(what)을 해야 하는지에 집중하며, 어떻게(how) 할지를 명시하지 않음

- [예시.js]

//함수형
let numbers = [ 1, 2, 3 ];

function muliply(numbers, multiplier) {
  for (let i = 0; i < numbers.length; i ++) {
    numbers[i] = numbers[i] * multiplier; 
  }
}
-------------------------------------------------------------
//선언형
function multiply(numbers, multiplier) {
  return numbers.map(n => n * multiplier);
}

2. 람다식

객체지향 언어인 자바에서 함수형 프로그래밍 기법을 지원하는 자바의 문법.
간결하게 코드 블록을 작성할 수 있는 표현 방법으로, 주로 함수형 인터페이스와 함께 사용.
자바는 새로운 함수 문법을 정의하는 대신, 이미 있는 인터페이스의 문법을 활용해서 람다식을 표현한다.

(매개변수) -> { 코드블록 }

- 함수형 인터페이스 (functional interface)

하나의 추상 메서드만 가지는 인터페이스.
@FunctionalInterface 어노테이션으로 함수형 인터페이스임을 명시할 수 있음.
→ 컴파일러가 두 개 이상의 추상 메서드를 선언하고 있으면 오류를 반환

예시 : Runnable, Callable, Consumer<T>, Supplier<T>, Function<T, R>

- 함수형 인터페이스에 정의된 메서드를 호출하는 방법

객체지향 프로그래밍 문법

case 1. 인터페이스의 구현 클래스를 만든 후, 해당 클래스의 생성자(new B)를 이용해서 객체를 생성하고, 객체의 참조 변수(case1)를 이용해서 메서드를 호출
(여러번 만들 때 사용)

case 2. 익명 inner 클래스를 사용해서 객체를 생성하고, 이 객체를 이용해서 메서드를 호출
(한번 만들 때 사용)

함수형 프로그래밍 문법

case 3. 람다식을 활용 : 익명 inner 클래스의 메서드 정의 부분만 가져와 메서드를 정의하고 호출 (화살표 함수) ---> 메서드가 하나여서 정의 부분만 가져와서 쓸 수 있는 것!

코드로 보는 예시

@FunctionalInterface
interface A {
    void method();
}

class B implements A {
    @Override
    public void method() {
        System.out.println("CASE1");
    }
}

public class Test {
    public static void main(String[] args) {
        // CASE1
        A case1 = new B();
        case1.method();
     
        // CASE2
        A case2 = new A() {
            @Override
            public void method() {
                System.out.println("CASE2");
            }
        };
        case2.method();
        
        // CASE3
        A case3 = () -> {
            System.out.println("CASE3");
        };
        case3.method();        
    }
}

3. 람다식 CASE

- 매개변수와 리턴 값이 없는 경우

void method() { System.out.println("hello"); }

() -> { System.out.println("hello"); }
() -> System.out.println("hello");

- 매개변수는 있고, 리턴 값이 없는 경우

void method(int i) { System.out.println(i); }

(int i) -> { System.out.println(i); }
i -> System.out.println(i); 

- 매개변수는 없고, 리턴 값은 있는 경우

int method() { return 100; }

() -> { return 100; }

- 매개변수, 리턴 값이 모두 있는 경우

int method(int a, int b) { return a + b; }

(int a, int b) -> { return a + b; }
(a, b) -> a + b;

코드로 보는 예시

// 매개변수 X, 반환값 X
@FunctionalInterface
interface XX {
    void method();  // public abstract void method();
}

// 매개변수 X, 반환값 O
@FunctionalInterface
interface XO {
    int method();   // 숫자 100을 반환
}

// 매개변수 O, 반환값 X
@FunctionalInterface
interface OX {
    void method(int i); // 매개변수에 10을 더한 수를 출력
}

// 매개변수 O, 반환값 O
@FunctionalInterface
interface OO {
    double method(int i, double d); // 매개변수 값들을 더한 결과를 반환
}


public class Lambda {
    static void caseXx() {
        class XXClass implements XX {
            @Override
            public void method() {
                System.out.println("XX1");
            }   
        }
        XX xx1 = new XXClass();
        xx1.method();
        
        XX xx2 = new XX() {
            @Override
            public void method() {
                System.out.println("XX2");
            }   
        };
        xx2.method();
        
        XX xx3 = () -> System.out.println("XX3");
        xx3.method();        
    }
    
    static void caseXo() {
        class XOClass implements XO {
            @Override
            public int method() {
                return 100;
            }            
        }
        XO xo1 = new XOClass();
        System.out.println(xo1.method());
        
        XO xo2 = new XO() {
            @Override
            public int method() {
                return 100;
            }  
        };
        System.out.println(xo2.method());
        
        XO xo3 = () -> 100;
        System.out.println(xo3.method());         
    }
    
    static void caseOx() {
        OX ox1 = i -> System.out.println(i + 10);
        ox1.method(100);    // 110
        
        OX ox2 = new OX() {
            @Override
            public void method(int i) {
                System.out.println(i + 10);
            }
        };
        ox2.method(100);
        
        class OXClass implements OX {
            @Override
            public void method(int i) {
                System.out.println(i + 10);
            }
        }
        OX ox3 = new OXClass();
        ox3.method(100);
    }
    
    public static void caseOO() {
        OO oo1 = (int i, double d) -> i + d;
        System.out.println(oo1.method(100, 10.0)); // 110.0
        
        OO oo2 = new OO() {
            @Override
            public double method(int i, double d) {
                return i + d;
            }
        };
        System.out.println(oo2.method(100, 10.0));
        
        class OOClass implements OO {
            @Override
            public double method(int i, double d) {
                return i + d;
            }
        }
        OO oo3 = new OOClass();
        System.out.println(oo3.method(100, 10.0));
    }
    
    public static void main(String[] args) {
        caseXx();
        caseXo();
        caseOx();
        caseOO();
    }
}

- 이미 구현되어 있는 인스턴스 메서드를 참조

객체참조변수::인스턴스메서드명 (단, 반드시 객체를 먼저 생성해야 함)

코드로 보는 예시

interface I {
    void iii(); // C 클래스의 ccc() 메서드를 호출
}

class C {
    void ccc() {
        System.out.println("ccc()");
    }
}

public class Lambda {
    public static void main(String[] args) {
        // 익명 이너클래스
        I i1 = new I() {
            @Override
            public void iii() {
                C c = new C();
                c.ccc();
            }
        };
        i1.iii();
        
        // 람다식
        I i2 = () -> {
            C c = new C();
            c.ccc();
        };
        i2.iii();
        
        // 메서드 참조로 변경
        C cc = new C();
        I i3 = cc::ccc;
        i3.iii();
    }
}

코드로 보는 예시 2

package com.test;

interface I {
    // 매개변수로 전달된 i 값을 출력
    void printNumber(int i);
}

public class Lambda {
    public static void main(String[] args) {
        // 구현 클래스 정의 후 인스턴스를 생성해서 실행
        class II implements I {
        	@Override
        	public void printNumber(int i) {
        		System.out.println(i);
        	}
        }
        I i1 = new II();
        i1.printNumber(100);
        
        // 익명 이너 클래스를 이용해서 실행 
    	 I i2 = new I() {
             @Override
             public void printNumber(int i) {
            	 System.out.println(i);
             }
         };
         i2.printNumber(100);
         
        // 람다식을 이용해서 실행
         I i3 = i ->  System.out.println(i);
         i3.printNumber(100);
         
        // 메서드 참조를 이용해서 실행 
         I i4 = System.out::println;
         i4.printNumber(100);
        
    }
}

- 정적 메서드를 참조

클래스이름::정적메서드이름

코드로 보는 예시

@FunctionalInterface
interface I {
    // 클래스 C의 smile() 메서드를 호출
    void printSmile();
}

class C {
    static void smile() {
        System.out.println("^_^");
    }
}

public class Lambda {
    public static void main(String[] args) {
        class IClass implements I {
            @Override
            public void printSmile() {
                C.smile();
            }
        }
        I i1 = new IClass();
        i1.printSmile();
        
        I i2 = new I() {
            @Override
            public void printSmile() {
                C.smile();
            }
        };
        i2.printSmile();
        
        I i3 = () -> C.smile();
        i3.printSmile();
        
        I i4 = C::smile;
        i4.printSmile();
    }
}

- 첫번째 매개변수로 전달된 객체의 인스턴스 메서드 참조

코드로 보는 예시

@FunctionalInterface
interface I {
    // 클래스 C의 print() 메서드를 이용해서 매개변수 i의 출력
    void printNumber(C c, int i);
}

class C {
    void print(int i) {
        System.out.println(i);
    }
}

public class Lambda {
    public static void main(String[] args) {
        class IClass implements I {
            @Override
            public void printNumber(C c, int i) {
                c.print(i);
            }
        }
        I i1 = new IClass();
        i1.printNumber(new C(), 100);
        
        I i2 = new I() {
            @Override
            public void printNumber(C c, int i) {
                c.print(i);
            }
        };
        i2.printNumber(new C(), 100);
        
        I i3 = (C c, int i) -> c.print(i);
        i3.printNumber(new C(), 100);
        
        I i4 = C::print;
        i4.printNumber(new C(), 100);
    }
}

코드로 보는 예시 2 (reverse)

@FunctionalInterface
interface I {
    // 매개변수로 전달된 s의 길이를 반환
    int stringLength(String s);
}

public class Lambda {
    public static void main(String[] args) {
        I i4 = String::length;
        System.out.println(i4.stringLength("hello, lambda"));   // 13
        
        I i3 = s -> s.length();
        System.out.println(i3.stringLength("hello, lambda"));
        
        I i2 = new I() {
            @Override
            public int stringLength(String s) {
                return s.length();
            }
        };
        System.out.println(i2.stringLength("hello, lambda"));
        
        class IClass implements I {
            @Override
            public int stringLength(String s) {
                return s.length();
            }
        }
        I i1 = new IClass();
        System.out.println(i1.stringLength("hello, lambda"));
    }
}

- 배열 생성자 참조

배열타입[]::new

코드로 보는 예시

import java.util.Arrays;

@FunctionalInterface
interface Arr {
    // 매개변수로 전달된 len 크기의 int[]를 반환
    int[] createArray(int len);
}

public class Lambda {
    public static void main(String[] args) {
        class ArrClass implements Arr {
            @Override
            public int[] createArray(int len) {
                return new int[len];
            }
        }
        Arr arr1 = new ArrClass();
        System.out.println(Arrays.toString(arr1.createArray(3)));   // [0, 0, 0]
        
        Arr arr2 = new Arr() {
            @Override
            public int[] createArray(int len) {
                return new int[len];
            }
        };
        System.out.println(Arrays.toString(arr2.createArray(3)));
        
        Arr arr3 = len -> new int[len];
        System.out.println(Arrays.toString(arr3.createArray(3)));
        
        Arr arr4 = int[]::new;
        System.out.println(Arrays.toString(arr4.createArray(3)));
    }
}

- 클래스 생성자 참조

클래스명::new

코드로 보는 예시

@FunctionalInterface
interface RefDefaultConstructor {
    // Cls 클래스의 Cls() 생성자를 이용해서 인스턴스를 생성해서 반환
    Cls getInstance();
}

@FunctionalInterface
interface RefParamConstructor {
    // Cls 클래스의 Cls(int i) 생성자를 이용해서 인스턴스를 생성해서 반환
    Cls getInstance(int i);
}

class Cls {
    Cls() {
        System.out.println("첫번째 생성자");
    }
    Cls(int i) {
        System.out.println("두번째 생성자");
    }
}

public class Lambda {
    public static void main(String[] args) {
        RefDefaultConstructor r1 = new RefDefaultConstructor() {
            @Override
            public Cls getInstance() {
                return new Cls();
            }
        };
        r1.getInstance();       // 첫번째 생성자
        
        RefDefaultConstructor r2 = () -> new Cls();
        r2.getInstance();
        
        RefDefaultConstructor r3 = Cls::new;
        r3.getInstance();
        
        
        RefParamConstructor p1 = new RefParamConstructor() {
            @Override
            public Cls getInstance(int i) {
                return new Cls(i);
            }
        };
        p1.getInstance(100);    // 두번째 생성자
        
        RefParamConstructor p2 = i -> new Cls(i);
        p2.getInstance(100);
        
        RefParamConstructor p3 = Cls::new;
        p3.getInstance(100);
    }
}

4. 표준 API의 함수적 인터페이스

자바 표준 API 를 보면, 함수형 문법을 쓰는 케이스가 많다. 대표적인 케이스는 public interface Runnable이 있다. 이 외에도 더 많은 유틸을 보려면 java.util.function를 참고하면 된다.

코드로 보는 예시 (public interface Runnable)

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}




public class Lambda {
    public static void main(String[] args) {
        class MyRunnable implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 10; i ++) 
                    System.out.println("#1 : " + i);
            }
        }
        Runnable runner1 = new MyRunnable();
        
        Runnable runner2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i ++) 
                    System.out.println("#2 : " + i);
            }
        };
        
        Runnable runner3 = () -> {			⇐ 람다식으로 run() 메서드를 정의하고 실행 가능
            for (int i = 0; i < 10; i ++)
                System.out.println("#3 : " + i);
        };
        
        Thread thread1 = new Thread(runner1);
        Thread thread2 = new Thread(runner2);
        Thread thread3 = new Thread(runner3);
        
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

OUTRO

람다식의 다양한 사례를 공부할 수 있었습니다. 람다식 역시 현업에서 많이 다루지 않았는데, 코드에서 람다식이 나왔을 때 이제는 어렵지 않게 이해할 수 있을 것 같습니다.

profile
지니니

0개의 댓글