Modern JAVA 8 람다 (feat. Effectively Final)

Mugeon Kim·2024년 1월 7일
0

😶‍🌫️함수형 프로그래밍


Java는 객체지향 언어이기 때문에 기본적으로 함수형 프로그래밍이 불가능하다. 하지만 JDK8부터 Stream API와 람다식, 함수형 인터페이스 등을 지원하면서 Java를 이용해 함수형으로 프로그래밍할 수 있는 API 들을 제공해주고 있다.

First Class Citizon

  • First Class Citizon 은 아래의 속성들을 모주 만족해야 합니다.

• 변수에 값을 할당할 수 있어야 합니다.

• 함수의 파라미터로 넘겨줄 수 있어야 합니다.

• 함수의 반환값이 될 수 있어야 합니다.

https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html

 [Function (Java Platform SE 8 )

docs.oracle.com](https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html)

@FunctionalInterface


함수형 인터페이스

  • 추상 메소드가 딱 하나만 존재하는 인터페이스 이다.

💡 public abstract가 단 하나만 있으면 가능하며 다른 형태로 정의가 되어져 있는 것은 상관이 없다.

@FunctionalInterface
public interface PracticeLambda {
    void practice(); //public abstract 생략

    static void practice2() {
        System.out.println("연습1");
    }

    default void practice3() {
        System.out.println("연습2");
    }
}

만약에 추상 메소드가 2개 이상이면 @functionalInterface에서 컴파일 오류가 나온다.

자바에서 제공하는 함수형 인터페이스

  • Function<T,R> , UnaryOperator
    • 두개의 인자를 받아서 하나의 결과를 Return한다.
    • 여기서 두개의 인자의 타입은 T,R 서로 다르거나 같아도 상관이 없다.
    • 만약에 두개의 인자가 같은 타입이면 UnaryOperator를 사용

🦖현재의 이 코드는 Function 인터페이스를 익명객체 스타일로 구현을 했다.

public class PracticeLamdba {
    public static void main(String[] args) {
        Function<Integer, Integer> pricaticeTest = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return integer+10;
            }
        };
        
        System.out.println(pricaticeTest.apply(3));
    }
}
  • 값을 받아와서 apply를 오버라이드 해서 +10을 해주는 함수를 만들었다.

🦖이러한 익명의 느낌을 람다로 표현이 가능하다.

public class PracticeLamdba {
    public static void main(String[] args) {
        Function<Integer, Integer> pricaticeTest = integer -> integer+10;
        System.out.println(pricaticeTest.apply(3));
    }
}
  • 코드가 엄청 간결하고 가독성이 높아졌다.

Function<T,R> → apply()

→T는 입력하는 타입을 의미하며 R은 return 되는 값의 타입을 의미한다.

여기서 만약에 Funciotn 함수가 여러개 있을 때 Function의 다양한 기능을

사용할 수 있다.

  1. compose
  • 간단하게 코드를 보여주고 설명을 하겠다.
  public class PracticeLamdba {
  	public static void main(String[] args) {
  		Function<Integer, Integer> plus = (number) -> number+10;

 		 Function<Integer , Integer> multiple = number2 ->number2*2;

		  int result=plus.compose(multiple).apply(10);
		  System.out.println(result);
  }

더하기 기능과 곱하기 기능을 만들었고 copose를 사용을 했다.
이때 result의 값은 어떻게 나올까? —> 결과는 30이 나오게 된다.

Compose란
()의 기능을 먼저 연산을 하고 그 뒤에 첫번 째로 오는 기능을 연산을 한다.
multiple → 결과 → 결과 & plus 라고 생각을 하면 좋다.

public class PracticeLamdba {
    public static void main(String[] args) {
        Function<Integer, Integer> plus = (number) -> number+10;
        Function<Integer , Integer> multiple = number2 ->number2*2;
        int result=plus.andThen(multiple).apply(10);
        System.out.println(result);
    }
}

andThen은 compose와 반대로 먼저오는 기능을 연산하고 그 뒤에 () 기능을 연산을 하는 것을 의미한다.

즉. plus → 결과 → multiple을 하며 결과는 40이 나오게 된다.

  • BiFuntion<T,U,R>
    • BiFunction은 입력값이 2개를 받고 두개의 연산에 대한 결과값 R로
    • 반환하는 기능을 가진다.
    • 기본적인 기능은 Function<T,R>과 비슷하며 입력값이 2개라는 차이만 있다.

익명

public class PracticeLamdba {
    public static void main(String[] args) {
        BiFunction<Integer,Integer,Integer>biFunction = new BiFunction<Integer, Integer, Integer>() {
            @Override
            public Integer apply(Integer integer, Integer integer2) {
                return integer+integer2;
            }
        };
        int result=biFunction.apply(10 , 20);
        System.out.println(result);
    }
}

람다

public class PracticeLamdba {
    public static void main(String[] args) {
        BiFunction<Integer,Integer,Integer>biFunction = (integer, integer2) -> integer+integer2;
        int result=biFunction.apply(10 , 20);
        System.out.println(result);
    }
}

Function<T,R> & BiFunction<T,U,R> 같은 타입

💡 만약에 T ,U ,R이 다 똑같은 Integer 타입이면 다 적어야 되는가?

만약에 타입이 같으면 Function은 UnaryOperator를 사용하면 된다.

그러면 입력 , 출력값의 타입이 모두 T로 정해진다.

BiFunction<T,U,R>은 입력 출력 모두 같으면 BinaryOperator를 사용

public class PracticeLamdba {
    public static void main(String[] args) {
        UnaryOperator<Integer>unaryOperator = (n) -> n+10;
        System.out.println(unaryOperator.apply(10));

        BinaryOperator<Integer>binaryOperator=(a,b)->a+10+b;
        System.out.println(binaryOperator.apply(10,20));
    }
}
  • Consumer - apply()

T타입을 입력 받아서 아무값도 리턴하지 않는 함수 인터페이스를 의미

익명

public class PracticeLamdba {
    public static void main(String[] args) {
        Consumer<String>consumer = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println("내 이름은 "+s +"입니다.");
            }
        };
        consumer.accept("김무건");
    }
}

람다

public class PracticeLamdba {
    public static void main(String[] args) {
        Consumer<String>consumer = (name) -> System.out.println("내 이름은"+name+"입니다.");
        consumer.accept("김무건");
    }
}
  • Suppiler
    • T 타입의 값을 제공하는 함수 인터페이스
      • 인자가 필요 없다.
      • 내가 어떤 값을 받을지 결정
      • T get()

익명

public class PracticeLamdba {
    public static void main(String[] args) {
        Supplier<String> supplier = new Supplier<String>() {
            @Override
            public String get() {
                return "김무건";
            }
        };
        System.out.println("내 이름은 "+supplier.get());
    }
}

람다

public class PracticeLamdba {
    public static void main(String[] args) {
        Supplier<String> supplier = () -> "김무건";
        System.out.println("내 이름은 "+supplier.get());
    }
}
  • Predicate
    • T타입을 받아서 Boolean을 리턴하는 함수 인터페이스
    • 함수 조합용 메소드
      • and
      • or
      • negate()

익명

public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate<Integer>predicate = new Predicate<Integer>() {
          @Override
          public boolean test(Integer integer) {
              return integer%2==0;
          }
      };
        System.out.println(predicate.test(10));
    }
}

람다

public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate<Integer>predicate = integer -> integer%2==0;
        System.out.println(predicate.test(10));
    }
}

만약에 다양한 조건이 있다고 생각하면 and or를 이용하면 된다.

예를 들어서 짝수이면서 값이 4인 결과를 받고 싶다고 조건을 만들면

  1. 짝수
  2. 값이 4

이 두개의 조건을 and연산을 하면 되는데

and

public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate<Integer>integerPredicate = integer -> integer%2==0;
      Predicate<Integer>predicate = num ->num==4;
      
      if(integerPredicate.and(predicate).test(4)){
          System.out.println("참");
      }else System.out.println("거짓");
      
    }
}

or

public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate<Integer>integerPredicate = integer -> integer%2==0;
      Predicate<Integer>predicate = num ->num==4;
      
      if(integerPredicate.or(predicate).test(4)){
          System.out.println("참");
      }else System.out.println("거짓");
      
    }
}

negate()

public class PracticeLamdba {
    public static void main(String[] args) {
      Predicate<Integer>integerPredicate = integer -> integer%2==0;
      Predicate<Integer>predicate = num ->num==4;
        System.out.println(predicate.negate().test(4));
    }
}

Effectively Final

Java8에서 final이 붙지 않은 변수의 값이 변경되지 않는다면, 그 변수를 Effectively final이라고 합니다.

람다에서는 사용할 수 있는 로컬 변수는 Effectively Final만 사용이 가능하다.

public class EffectivelyFinal {
    public static void main(String[] args) {
        new Pint().go();
    }
}
class Pint {
    void go() {
        int baseAnInt = 111;
        Function<Integer, Integer> function = (num) -> num + baseAnInt;
        System.out.println(baseAnInt+"baseAnInt");
        System.out.println(function.apply(10)+"function");
    }
}

위에 코드를 보면 baseAnInt는 Effectively Final이다.

자바에서는 프로그램이 값이 변경하지 않는 수를 final로 추측을 할 수 있다.

여기서 람다에서 중요한 부분은 람다식에서 참조하는 로컬 변수의 값은 변경을 할 수 없다.

이 부분은 스코프의 내용을 이해하면 더 이해하기 쉽다. ==> 이 부분은 뒤에서 더 설명

현재 간단하게 설명을 하면 GO()메소드의 스코프와 람다의 스코프가 같다. 같은 스코프에서 동일한 변수를 지정할 수 없고 이러한 부분을 밑에 코드를 보면 알 수 있다.

스코프
먼저 스코프란 변수를 사용할 수 있는 범위

baseAnInt를 선언하고 람다식에서 사용하니 이미 스코프에서 정의가 되어져 있다고 나온다.

이것을 통해서 람다는 go메소드랑 동일한 스코프를 가지고 있다고 이해할 수 있다.

람다? Effectively Final?

람다식에서 사용되는 변수는 해당 변수를 의미하는 것이 아닌 복사본이다.

근본적인 이유는 생명 주기가 다르기 때문이다.

1. 로컬 변수를 생각을 해보면 메소드가 실행이 되고 끝나면 생명주기는 끝이 난다.

하지만 람다는 고차 함수여서 람다 자체를 인자로 받고 리턴을 할 수 있기 때문에 람다의 생명주기는 여전히 실행을 하고 있을 수 있다.

즉. 로컬 변수의 생명 주기가 끝나도 람다는 로컬 변수의 값을 복사하여 가지고 있어야 한다.

https://incheol-jung.gitbook.io/docs/study/kotlin-in-action/8

 [8장 고차 함수: 파라미터와 반환 값으로 람다 사용 - Incheol's TECH BLOG

코틀린 표준 라이브러리의 컬렉션 함수는 대부분 람다를 인자로 받는다. filter와 map은 인라인 함수다. 따라서 그 두 함수의 본문은 인라이닝되며, 추가 객체나 클래스 생성은 없다. 하지만 이 코

incheol-jung.gitbook.io](https://incheol-jung.gitbook.io/docs/study/kotlin-in-action/8)

2. 로컬 변수와 람다의 Thread를 이해하자

  • 로컬 변수

    • JVM에서 Stack에 저장이 된다.
    • Stack에 따른 Thread가 배정이 되고 Thread 종료는 로컬 변수의 끝을 의미한다
  • 람다의 쓰레드

    • 람다는 별도의 Thread를 가진다.
    • 만약에 Stack에 직접 접근이 가능하여 참조하고 있다고 한다면 이것은 멀티 쓰레드에 위험
    • 람다가 참조한 변수의 생명 주기는 람다가 있으면 보장이 된다.

스코프

https://wakestand.tistory.com/179

 [자바 변수의 스코프가 뭔말?

면접 시 많이 물어보는 것이 변수의 스코프인데 스코프가 뭔 말인지 감이 안와서 어려울 수 있는데 막상 보면 단순하다 먼저 스코프란 변수를 사용할 수 있는 범위를 얘기하는데 {} 안에서 변수

wakestand.tistory.com](https://wakestand.tistory.com/179)

기본적인 스코프의 내용은 링크를 통해서 확인이 가능하다.

이 페이지에서 설명하는 스코프는 람다에서 람다 , 로컬 클래스 , 익명 클래스에 대한 스코프를 비교하고

람다에 대한 특징을 알아보기 위해 작성을 하고 있다.

class Pint {
    private int baseAnInt = 111;

    class LocalClass{
        int baseAnInt = 1;
        void localClass(){
            System.out.println(baseAnInt);//1
        }
    }

    Consumer<Integer>consumer = new Consumer<Integer>() {
        int baseAnInt=2;
        @Override
        public void accept(Integer integer) {
            System.out.println(baseAnInt);//2
        }
    };

    void go() {
        Function<Integer, Integer> function = (baseAnInt) -> baseAnInt + baseAnInt;//111
    }
}

로컬 클래스와 , 익명 클래스는 각각 baseAnInt의 값을 변경을 할 수 있지만 람다식은 변경이 불가능 하다.

이것을 통해 위에 2개와 람다식은 서로 다른 스코프를 통해 동작을 한다는 것을 알 수 있다.

이것에 대한 자세한 내용은 밑에 블로그를 참고

https://tjdtls690.github.io/studycontents/java/2022-10-24-lambda_anonymous_local_class_difference/

 [[자바, Java] 람다 (Lambda) - 쉐도잉 (Shadowing)

.

tjdtls690.github.io](https://tjdtls690.github.io/studycontents/java/2022-10-24-lambda_anonymous_local_class_difference/)

profile
빠르게 실패하고 자세하게 학습하기

0개의 댓글