본문 바로가기

Java/Study

[JAVA]백기선 라이브 스터디 15주차 :람다식

목표

자바의 람다식에 대해 학습하세요.

학습할 것 (필수)

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

👉함수형 프로그래밍

함수형 프로그래밍은 패러다임이다. 람다 표현식은 이를 나타내는 수단이다!

람다식은 함수형 프로그래밍에 근간을 두기에 정리하고 넘어 가고자 한다.

함수의 입력만을 의존해 출력을 만드는 구조로 외부 상태를 변경하는 것을 지양한다.

side effect를 제거하면 코드를 프로그램의 동작을 이해하고 예측하는 것이 훨씬 쉽다.(너무나도 공감..)

side effect → 변수 수정, 필드 변경, 입력값 받기, 예외 던지기, 콘솔 출력, 파일 입력, 출력 등

  • 순수함수(Pure Function)

같은 인자에 동일한 반환값을 갖는다.

함수의 실행이 외부 상태의 변경에 영향을 미치지 않는다. 순수한 함수는 멀티쓰레드 환경에서도 안전하고, 병렬 처리가 가능하다.

  • 익명 함수

리터럴 방식으로 만들어진 이름이 없는 함수를 의미한다.

함수 리터럴 방식

함수를 문자 그래로 식에 담으면 리터럴 방식이다.

var funcA = function(name){
     alert(name + "님, 반갑습니다.");
}

익명 함수를 왜 만들까? → 함수를 재사용하지 않을 경우 익명 함수를 만들어 사용한다.

  • 고계함수

함수도 하나의 값으로 취급하고, 함수의 인자로 함수를 전달할 수 있는 특성이 있다.

이러한 함수를 일급함수라고 한다.

일급 함수 조건

  1. 함수가 객체로 취급된다.
  2. 함수 객체를 인자로 넘긴다.
  3. 함수 객체로 결과 값을 반환한다.

💡함수형 프로그래밍의 컨셉

  1. 변경 가능한 상태의 것을 불가능한 상태로 만들어 side effect를 없애자.
  2. 모든 것을 객체로 간주한다.
  3. 코드를 간결하게 가독성 높게 작성한다.
  4. 동시성 작업을 보다 안전하게 구현하자.

람다식 사용법

자바 람다 표현식의 기본적인 구조

(int a, int b) -> {a + b} // 

특징

단순한 람다구문의 경우 중괄호가 생략 될 수 있다.

타입 추론이 가능하므로 타입을 선언하지 않아도 된다.

컴파일러가 익명클래스로 람다식을 변환한다.

→ 함수형 인터페이스(추상 메서드 한개 있는 구조)를 컴파일러가 구현

리턴이 없을 수도 있다.

() -> {}
() -> 26
() -> null
() ->{return 26;}
() -> {
if(true){return 26;}
else{return 0;}
}
(int a) -> a + 2
(String s) -> s.length()
s -> s.length()
(String s) ->s.length()
(Thread t) -> t.start() //single declared type parameter
(final int y)-> y + 1 

interface Cal2{
    int cal(final int y);
}
public class DemoFunctional {
    public static void main(String[] args) {
        Cal2 running = (final int y) -> y + 1;
}

// interface에 정의된 매개 변수를 수정할 수 없음
interface Calculator{
    int cal(int x, int y);
}
(int x, final int y) -> x + y // 에러

// 유추된 유형과 선언된 유형을 혼합할 수 없음
interface Calculator{
    int cal();
}
(int x, y) -> x + y

아래에서 설명된 함수형 인터페이스를 기반으로 람다식을 활용해 보면 이해가 더 쉽다.

함수형 인터페이스

Java 에도 함수형 프로그래밍을 위한 인터페이스가 추가 되었다.

함수형 인터페이스는 추상메서드가 단,하나 존재하는 인터페이스다.

함수형 인터페이스 + 람다식으로 표현 함으로

⇒ 입력에 의해서만 출력이 결정 되도록(순수 함수)

⇒ 함수형 인터페이스 메소드에서 또다른 함수형 인터페이스를 인자로 받는다.(익명 함수, 고계 함수)

public interface Functional1 {
  boolean accept();
}

public interface Functional2 {
  boolean accept();
  default boolean reject() { return !accept(); }
}

@FunctionalInterface
public interface Functional3 {
  boolean accept();// 추상 메서드가 하나 더 있으면 컴파일러 에러 발생 !
}

public interface NotFunctional {
  boolean accept();
  boolean reject(

Variable Capture

scope : 예전에 변수의 scope에 대해 정리했던게 생각났다.

로컬클래스, 익명 클래스, 람다식은 변수의 활용 범위 (scope)가 다르다.

  1. nested class 별 차이점
  2. 람다 내부에서 접근가능한 변수

nested class

public class ThreeTypeOfScope {
    public static void main(String[] args) {
        ThreeTypeOfScope threeScope = new ThreeTypeOfScope();
        threeScope.run()

    }
    private void run() {
    //    final int baseNumber = 10;
        int baseNumber = 10;
//============================================================================================
        //로컬클래스
        class LocalClass{
            void printNumber(){
//                baseNumber++;// 에러남 - final로 선언한 변수를 수정하려고 하니깐
                int baseNumber = 5;
                System.out.println(baseNumber);//5
            }
        }
//=============================================================================================
        //익명클래스
        Consumer<Integer> integerConsumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
//                baseNumber++;// 에러남
                int baseNumber = 5;
                System.out.println(baseNumber);
            }
        };
//=============================================================================================
        IntConsumer printInt = value -> {
            // 람다식에 사용한 변수는 final 혹은 effective final 이라함
//            int baseNumber = 5;컴파일 에러 
            System.out.println(value + baseNumber);
        };
        printInt.accept(5);
    }
}

대부분의 람다식은 body 안에 주어진 변수만 활용하지만, 로컬 클래스와 익명 클래스 처럼 외부의 변수 (free variables)도 활용 할 수 있다.

"Lambda Capturing" 이란 외부 변수를 활용하는 것을 의미한다.

람다에서 사용가능 한 변수는

1) final 이거나

2) final 의 속성을 띈 effective final 이어야 한다.

또한, Capturing 이라는 의미 답게 참조는 가능하나 변경은 불가능 하다 !

접근 가능한 변수: 지역변수(수정X), static 변수, 멤버 변수

쓰레드도 stack 영역에 생긴다. 하나의 쓰레드는 내부적으로 별개의 메모리 구조 static, stack, heap영역을 갖게 되고, 이런 이유로 하나의 쓰레드는 다른 쓰레드로 접근 할 수 없지만, static 영역과 heap 영역은 공유해서 사용 할 수 있다.

그래서 static 변수와 멤버 변수는 수정이 가능하지만

지역 변수는 stack 메모리에 올라가고, stack 메모리에 올라간 변수는 다른 쓰레드에 의해 실행이 완료 되어 stack에 남아 있지 않을 수 있다.

그래서 수정이 불가능하고 컴파일러가 에러를 낸다.

"capture"의 의미가 값을 참조한다는 것을 잘 나타내주는 예시인거 같다.

메소드, 생성자 레퍼런스

람다식을 더 줄일 수 있는 방법들이다.

  1. class 내부에 있는 static method 경우
  2. class 내부에 있는 일반 method 경우
  3. 지역 변수에 할당된 object가 있는 경우
class Student{
    private int age;
    private String name;

    public static void setAge(int age) {
    }

    public static void setName(String name) {
    }

}

public class MethodReference {
    Student stu1 = new Student();
    Student stu2 = new Student();

    public void demoConstructor(){
        Consumer<String> setName1 = name ->stu1.setName(name);
        Consumer<Integer> setAge1 = age->stu1.setAge(age);


                // 메소드 레퍼런스
        Consumer<String> setName2 = Student::setName;
        Consumer<Integer> setAge2 = Student::setAge;

    }

}

생성자 레퍼런스

생성자 역시 래퍼런스로 생성할 수 있다.

특징

import java.util.function.Function;
import java.util.function.Supplier;

class Menu{
    private String name;

    public Menu(String s) {
        System.out.println("m2생성되었습니다");
    }

    public Menu() {
        System.out.println("m1생성되었습니다");
    }
}
public class ConstructorReference {
    public static void main(String[] args) {
        Supplier<Menu> coffee = Menu::new;// 객체의 생성x
        System.out.println("Menu 예상 생성 시점");
        Menu m1 = coffee.get();// 실제 객체 생성
        Function<String, Menu> tt = Menu::new;// 객체의 생성x성
        System.out.println("Menu 예상 생성 시점");
        Menu m2 = tt.apply("Tea");// 실제 객체 생성
    }
}

new 를 할 때가 아닌 Supplier.get() 이나 Function.apply() 시점에 객체가 생성 되므로

LAZY INITIALIZE 가능 !!!

lazy loading 과 비슷한 개념이라고 한다.(객체 A, B, C가 있는데 A 현황만 궁금한 경우 B, C의 데이터를 생성하는 것은 비효율 적이다.)

지연 초기화를 적절히 사용하면 불필요한 메모리의 소모를 줄일 수 있다.

이팩티브 자바 아이템73 에 주의점도 나온다.

👉성능 최적화를 위해 지연 초기화를 사용하는데 클래스를 초기화하고 생성하는 비용은 줄어들지만 필드 사용 비용은 증가시키므로 되도록이면 사용하지 말라고 한다.