본문 바로가기
Language/Java

[Java 8] 람다(Lambda) - 2

by ocwokocw 2021. 2. 10.
- 출처: Java 8 in action
- 출처: https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Closures
- 기본형 특화
만약 Generic으로 정의한 Predicate 를 사용할 때, 기본형 특화의 개념을 몰라도 큰 문제는 되지 않는다. 
Integer 는 int 를 감싼 래퍼클래스이며 int 형을 Integer로, Integer를 int 로 전환할 때 박싱과 언박싱의 과정을 거치게 된다. 문제는 이때 비용이 발생한다는 것이다. 물론 사람이 체감할때는 아주 적은 비용이나 데이터의 크기가 커진다면 영향을 끼칠 수도 있다.
1
Integer toWrapper = 1;
위와 같은 코드에서 1은 Integer로 오토박싱이 된다. 이 얘기는 아래와 같이 Predicate<Integer> 를 사용하면 오토 박싱이 일어난다는 얘기가 된다.
1
2
3
4
5
	Predicate<Integer> isPositiveNum = (Integer i) -> i > 0;
	
	if(isPositiveNum.test(1)) {
		System.out.println("Yes");
	}
자바에서는 박싱, 언박싱의 과정없이 함수형 인터페이스를 사용할 수 있도록 몇 가지 기본형에 대한 함수형 인터페이스들을 제공한다.
1
2
3
4
5
	IntPredicate isPositiveNumPrimitive = (int i) -> i > 0;
	
	if(isPositiveNumPrimitive.test(1)) {
		System.out.println("Yes");
	}
제네릭 부분없이 기본형인 int 를 사용하였다. 이 외에도 IntConsumer, IntFunction, LongConsumer, LongFunction, LongPredicate.... 와 같은 많은 기본형 특화 함수형 인터페이스를 제공하므로 성능에 이슈가 되거나 필요하다면 설명을 참조하여 사용하면 된다.
- 함수형 인터페이스와 Checked Exception
Checked Exception vs Unchecked Exception
Exception 에는 Checked Exception과 Unchecked Exception 이 있다. 파일이나 Resource 에 관한것을 다루려고 코드를 작성하다보면 빨간줄이 뜨면서 메소드 시그니처에 throw 를 하거나 try catch 로 잡아서 처리하라고 강제하는 부분을 보적이 있을 것이다. 이렇게 컴파일러가 체크하여 해당 Exception 처리를 강제 한다면 이는 Checked Exception 이다.
프로그램을 실행하다가 잘못 작성된 코드가 내가 예측하지 못한 상황이 일어나서 NPE(Null Pointer Exception)을 만난적이 있을 것이다. 해당 Exception에 컴파일러가 처리를 강제 하지 않았지만 프로그램 구동중에 일어날 수 있는 Exception은 Unchecked Exception 이다. 내가 Exception을 정의할때 RuntimeException을 상속받거나 어떤 Exception이 RuntimeException을 상속받는다면 컴파일러가 처리를 강제 하지 않는다.
Checked Exception인 IOException과 UncheckedException인 NullPointerException
1
2
3
4
5
public
class IOException extends Exception 

public
class NullPointerException extends RuntimeException
함수형 인터페이스는 Checked Exception 을 던지는걸 허용하고 있지 않다. 그래서 만약 Checked Exception 을 던지는 상황이라면 try catch 블록으로 감싸거나 던져야 한다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void main(String[] args) {
	
	
	Function<BufferedReader, String> f = (BufferedReader b) -> {
		try {
			return b.readLine();
		} catch (IOException e) {
			throw e;
		}
	};
	
}
위와 같은 코드를 작성하면 throw e; 부분에 Unhandled exception type IOException 메시지와 함께 컴파일이 되지 않을것이다. 
1
2
3
4
5
6
7
	Function<BufferedReader, String> f = (BufferedReader b) -> {
		try {
			return b.readLine();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	};
컴파일이 되게 하려면 컴파일러가 체크하지 않게 Unchecked Exception은 RuntimeException 으로 감싸서 던지도록 하면 된다.
- 형식추론
아래 코드를 한번 살펴보자.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class People {

	private String name;
	private Integer age;
	public String getName() {
		return name;
	}
	public Integer getAge() {
		return age;
	}
	
}
1
2
3
4
5
	//형식 추론 X
	Comparator<People> c1 = (People p1, People p2) -> p1.getAge().compareTo(p2.getAge());
	
	//형식 추론
	Comparator<People> c2 = (p1, p2) -> p1.getAge().compareTo(p2.getAge());
c1 은 People 로 형식을 포함하여 형식 추론을 하지 않으며, c2 는 제네릭에서 형식을 추론한다. 형식을 꼭 써야 한다 쓰지 말아야 한다는 규칙은 없으며, 가독성이 좋은 방향으로 람다를 작성하도록 하자.
- 지역변수
람다 외부에서 선언한 변수를 사용하는 개념에 대해서 설명하려고 한다. 자바스크립트에서 클로저의 개념을 공부해본적이 있는사람이라면 이해를 잘할 수 있을것이다.
1
2
3
4
5
6
7
	int portNumber = 8080;
	
	Runnable r = () -> {
		System.out.println(portNumber);
	};
	
	portNumber = 80;
위의 코드는 뭔가 문제가 없어보이지만 실제로 작성해보면 컴파일조차 되지 않는 코드이다.
인스턴스는 힙에 저장되며 지역변수는 스택에 저장된다. 외부변수를 할당한 스레드가 #1 이고, 람다가 실행되는 스레드가 #2 라고 할 때, #2가 변수에 접근할 때 #1이 종료될 수도 있다. 이때 접근이 가능하도록 변수를 직접 참조하는 것이 아니라 변수에 대한 복사본을 전달한다. 복사본의 값이 바뀌지 않아야 하므로 위와 같은 제약이 생긴다.
기술적인 이유를 떠나서 함수형 프로그래밍 사상에서 외부의 변수나 파라미터가 함수 내부에서 변경 된다는 것은 좋은일이 아니다. 흔히 void createSomeStr(List<String> list); 와 같은 함수 시그니처에서 list 에 대해 .add나 .remove 같은 연산으로 list 를 변경하곤 한다. 하지만 이렇게 되면 코드를 사용하는측에서 해당 함수의 내용을 완벽하게 파악하고 있어야 파라미터로 넘길 수 있게 된다.
- 클로저
Javascript 를 해본사람이라면 클로저개념에 익숙할것이다. 아래 코드를 살펴보자.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var Person = (function(){
    var _name = "";

    return {
      
        setName: function(name){
            _name = name;
        },
        getName: function(){
            return _name;
        }
    };
})();
위처럼 코드를 작성하면 _name을 private 변수처럼 사용할 수 있다. setName과 getName 에서 부모범위의 _name 변수에 값을 할당하며 접근이 가능하다. 그러나 Java 의 Lambda 에서는 변수에 접근만 가능하다. 클로저에 대해 더 관심이 있다면 이 글의 상단 MDN 의 클로저를 참조하라.

'Language > Java' 카테고리의 다른 글

[Java 8] Stream - 1 (Stream 개요)  (0) 2021.02.11
[Java 8] 람다(Lambda) - 3  (0) 2021.02.11
[Java 8] 람다(Lambda) - 1  (0) 2021.02.10
[Java 8] 객체로서의 함수  (0) 2021.02.10
[Java 8] 함수형 프로그래밍 개요  (0) 2021.02.10

댓글