[배경]
자바를 공부하면서 람다 표현식에 대해 알게 되었는데, 버전이 java8로 넘어가면서 새로 추가가 되었다고 해서 java8에는 또 어떤것이 달라졌는지, 어떤 메소드들이 추가되었는지 알아보자.
들어가기에 앞서 먼저 Java 8의 핵심을 요약하면 "함수형 프로그래밍 패러다임의 지원" 이다. 여기서
함수형 패러다임은 뭘까??
보통 객체지향 패러다임 에서는 객체 스스로가 상태를 가지고 있고, 객제간에 에시지를 전달하면서 협력하게 된다. 객제지향 프로그래밍의 경우, 클래스 디자인과 객체들의 관계를 중심으로 코드작성이 이루어진다. 따라서 상태, 멤버변수, 메서드 등이 긴밀한 관계를 가지고 있다. 특히 멤버변수가 어떤 상태를 가지고있는가에 따라 결과가 달라진다.
하지만 함수형 패러다임 에서는 작은 단위의 함수들이 모여 저리된다. 함수들은 외부와의 관계는 없고 단지 함수 자신만으로 존재한다. 함수형 프로그래밍의 경우, 값의 연산 및 결과 도출 중심으로 코드작성이 이루어진다. 함수 내부에서 인자로 받은 값을 별도로 저장하거나 하지 않고, 간결한 과정으로 저리하고 매핑하는데에 주 목적을 둔다.
그래서 함수형 프로그램밍의 특징을 정리하자면 다음과 같다.
1. 순수함수
- 함수에 동일한 인자를 패스했을 때 항상 같은 값을 반환하는 함수이다.
- 함수는 외부의 어떤 값도 수정할 수 없어야 한다.
2. 불변성(Immutability)
- state가 바뀌지 않아야 한다.
- state의 불변성을 지키기 위해 state를 복사해서 새로운 state를 만들고 조작한 반환한다.
3. 멱등성(Idempotent)
- 함수를 여러번 호출하더라도 항상 연산 결과는 같아야 한다. 즉 예상한 대로 결과값을 반환해야한다,
4. 명령형 vs 선언형
명령형 코드(imperative code)
- 기계에서 어떻게 수행해야(how to do) 할지 알려주는 코드이다.
- 기계어는 명령형이다.
for(let i = 0; i < 1000; i++) {
console.log(i)
}
- for반복문은 초기식, 조건식, 증강식을 직접 명시해야 하므로 명령형 코드에 가깝다.
선언형 코드(imperative code)
- 기계에게 무엇을 수행해야(what to do) 할지 알려주는 코드이다.
- high level language는 명령형에 가깝다.
[1, 2, 3].forEach(item => console.log(item))
- forEach문은 반복을 돌면서 무엇을 수행해야하는지 알려주는 코드로 선언형 코드에 가깝다.
함수형 프로그래밍과 선언형 코드
- 함수형 프로그래밍의 코드는 선언형 코드에 가깝다. 함수들을 서로 조합하고 구성하면서 프로그램에게 어떻게 수행하는지를 알려주는 것 보다 무엇을 수행해야하는지 알려준다.
- 선언형 코드는 결국 기계어처럼 명령형 코드로 컴파일 된다.
그래서 Java8 부터 새로 생긴 여러 가지 특징에 대해 알아보자.
[내용]
1. Lambda 표현식(expression)
익명 클래스를 사용하면 가독성도 떨어지고 불편한데, 이러한 단점을 보완하기 위해 람다 표현식이 만들어졌다. 대신, 이 표현식은 인터페이스에 메소드가 "하나"인 것들만 적용 가능하다. 그래서, 람다 표현식은 익명 클래스로 전환이 가능하며, 익명 클래스는 람다 표현식으로 전환이 가능하다.
Java에 있는 인터페이스 중, 메소드가 하나인 인터페이스에는 어떤 것들이 있을까?
- java.lang.Runnable
- java.util.Comparator
- java.io.FileFilter
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
다음의 예제를 보자.
(int x, int y) -> x + y
() -> 43
(String s) -> {System.out.println(s)};
기본 람다 표현식은 3 부분으로 구성되어 있다.
매개 변수 목록 | 화살표 토큰(Arrow Token) | 처리 식 |
(int x, int y) | -> | x + y |
좌측에는 넘겨지는 매개 변수들의 타입이 선언되고, 중간에는 화살표 연산자, 가장 우측에는 리턴되는 값을 표시한다. 즉, x와 y 값을 받아서 x+y를 리턴해 준다는 의미다.
다음의 예제를 보자.
interface Calculate {
int operation(int a, int b);
}
이렇게 Calculate라는 인터페이스가 있고, operation() 메소드가 선언되어 있다. 대신 a와 b로 무슨 작업을 하는지는 선언되어 있지 않다. 이 인터페이스를 익명 클래스로 구현하면 다음과 같다.
private void calculateClassic() {
Calculate calculateAdd = new Calculate() {
@Override
public int operation(int a, int b) {
return a + b;
}
};
System.out.println(calculateAdd.operation(1, 2));
}
operation() 메소드에는 간단하게 a와 b를 더하고 리턴해준다.
이 calculateAdd라는 익명 클래스 객체를 람다 표현식으로 처리하려면 어떻게 해야 할까? 다음의 코드를 보자.
private void calculateLambda() {
Calculate calculateAdd = (a, b) -> a + b;
System.out.println(calculateAdd.operation(1, 2));
}
코드가 엄청 간단해진 것을 볼 수 있다.
메소드의 첫 번째 줄을 보면,
(a, b) -> a + b;
라고 되어 있다. 이 메소드 내에는 a와 b라는 변수가 전혀 선언되어 있지도 않은데, 이렇게 a와 b를 사용하고 그 값을 더하기까지 하고 있다. 이 Calculate라는 인터페이스는 메소드가 하나만 선언되어 있기 때문에, (a, b)라고 되어 있는 부분은 operation() 메소드의 int a와 int b를 매개 변수로 받는다는 의미이다. 그리고, -> 옆에 a + b는 결과로 a와 b의 합을 리턴한다는 의미다. 따라서 이 메소드를 수행하면 3이라는 결과가 출력된다.
여기서 a와 b처럼 변수 이름은 임의로 선언해도 전혀 문제가 없다. 이 이름들을 x, y로 해도 전혀 이상 없이 수행된다.
빼기를 처리하는 람다 표현식도 작성할 수 있다.
private void calculateLambda() {
Calculate calculateAdd = (a, b) -> a + b;
System.out.println(calculateAdd.operation(1, 2));
Calculate calculateSubstract = (a, b) -> a - b;
System.out.println(calculateSubstract.operation(1, 2));
}
a에서 b를 빼는 람다 표현식을 구현했으며, 이 메소드의 수행 결과는 당연히 3과 -1이 출력된다.
다시 Calculator 인터페이스를 보자.
interface Calculate{
int operation(int a, int b);
}
일반적인 인터페이스이지만, 이 인터페이스는 Functional(기능적) 인터페이스라고 부를 수 있다. 기능적 인터페이스는 이와 같이 하나의 메소드만 선언되어 있는 것을 의미한다. 그런데, 이렇게만 선언해두면 매우 혼동될 수도 있다. 왜냐하면, 같이 개발하는 다른 친구가 이 인터페이스 선언이 모호하다며 operationAdd()와 operationSubstract() 메소드로 구분하여 두 개의 메소드를 선언할 수도 있기 때문이다.
interface Calculate {
int operationAdd(int a, int b);
int operationSubstract(int a, int b);
}
만약 이렇게 된다면 람다 표현식을 사용할 수 없고, 람다 표현식에 컴파일 오류가 발생한다.
java: incompatible types: java8.Test.Calculate is not a functional interface
multiple non-overriding abstract methods found in interface java8.Test.Calculate
이러한 혼동을 피하기 위하여, 인터페이스 선언시 어노테이션을 사용할 수 있다.
@FunctionalInterface
interface Calculate{
int operation(int a, int b);
}
명시적으로 이렇게 @FunctionalInterface를 사용하면 이 인터페이스에는 내용이 없는 "하나"의 메소드만 선언할 수 있다. 만약 두 개의 메소드를 선언한다면 다음과 같은 컴파일 오류가 발생한다.
java: Unexpected @FunctionalInterface annotation
java8.Test.Calculate is not a functional interface
multiple non-overriding abstract methods found in interface java8.Test.Calculate
메소드가 두 개 선언되어 있기 때문에 Functional 인터페이스가 아닌데 왜 어노테이션을 사용했냐고 이와 같이 컴파일 에러가 발생한다.
쓰레드를 처리하기 위한 Runnable 인터페이스를 통해 조금 더 람다 표현식에 친해져 보자.
public void runCommonThread(){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
new Thread(runnable).start();
}
익명 클래스로 Runnable의 run() 메소드를 이와 같이 구현해 놓았다. Runnable은 run() 메소드밖에 없기 때문에 람다 표현식으로 처리가 가능하다.
private void runThread(){
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}).start();
}
그런데 이 메소드는 run()에서 처리하는 것이 한 줄이기 때문에 더 간단히 표현할 수 있다.
private void runThreadSimple(){
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
}
중괄호를 없애고, 출력문 뒤의 세미콜론을 지웠다. 이렇게 해도 정상적으로 컴파일되고 수행된다.
정리하자면,
- 메소드가 하나만 존재하는 인터페이스는 @FunctionalInterface로 선언할 수 있으며, 이 인터페이스를 람다 표현식으로 처리할 수 있다.
- (매개 변수 목록) -> 처리식으로 람다를 표현하며, 처리식이 한 줄 이상일 때는 처리식을 중괄호로 묶을 수 있다.
[결론]
Java8 의 등장으로 함수형 프로그래밍 패러다임의 지원에 대해 알 수 있었고 함수형 프로그램밍의 정의인 값의 연산 및 결과 도출 중심으로 코드작성와 4가지 특징을 알수 있었다. 그래서 Java8 에서 새롭게 등장한 람다 표현식의 사용으로 익명 내부 클래스를 사용하는 대신 람다 표현식을 사용하여 코드가 짧고 가독성이 좋아졌다. java 8 로 변경으로 개발자들의 유지보수가 더 쉽고 효율적인 코딩에 가까워 진거 같다. 다음에는 또 어떤부분들이 바뀌었는지 알아봐야 겠다.
[출처 및 참조]
- 자바의 신
- https://velog.io/@jsj3282/Java-8%EC%97%90%EC%84%9C-%EB%B3%80%EA%B2%BD%EB%90%9C-%EA%B2%83%EB%93%A4%EC%9D%80
- http://kbs0327.github.io/blog/technology/java8-default-interface/
- https://bcp0109.tistory.com/entry/Java-8-%ED%95%A8%EC%88%98%ED%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-Functional-Interface
'java' 카테고리의 다른 글
Java8에 새로 추가된 것은 뭐가 있을까 ?_2 (funtional 패키지, Stream, 디폴트 메서드 등) (2) | 2024.06.10 |
---|---|
jAVA11에 새로 바뀐것들을 알아보자. (0) | 2024.05.29 |
G1GC에 대해 알아보자 (0) | 2024.04.30 |
[JVM] JVM 가장 기본적인 동작 과정 (0) | 2024.04.10 |