Java8 부터 Java도 lamda 표현식을 사용할수 있게 되었습니다.

Java lamda

public class RunnableImpl implements Runnable{

		int num = 0;
		
		@Override
		public void run() {
			
			num++;
			System.out.println("num : " + num);
		}
		
}

Runnable r = new RunnableImpl();

Runnable r2 = new Runnable() {
		
	@Override
	public void run() {
		System.out.println("run r2");			
	}
};

새로운 Runnable 객체를 생성하는 예제입니다. Runnable interface 를 구현한 방식과 익명 클래스 방식으로 구현해 보았습니다. lamda 표현식의 특징은 클래스명이나 method 명을 알고있을 필요가 없다는 것입니다.

Runnable ex = () -> {
	System.out.println("run")
};

lamda를 사용하니 훨씬 간결해지고 명확해 졌습니다. 더 간단히 하면 이렇게 표현 할 수 있습니다.

Runnable ex = () -> System.out.println("run");

functional programming

앞에서 간단한 Runnable 예제를 통해서 lamda를 맛보았습니다.
lamda 표현식을 좀더 알아가기 전에 functional programming에 대해서 알아보겠습니다.

functional programming은 함수의 입력만을 의존하여 출력을 만드는 구조로 외부에 상태 변경을 지양하는 패러다임을 말합니다.

  • Pure Function
  • Annonymous Function
  • Higher-order Function(고계함수)

이러한 3가지 조건을 만족하는 것을 functional programming 이라고 말할수 있습니다. Pure Function 은 side-effect 가 없는 함수로 , 함수의 실행이 외부에 영향을 끼치지 않는 함수를 말합니다. Annonymous Function은 이름없는 함수를 정의 할수 없어야 한다는 것 입니다. Higher-order Function은 함수를 다루는 함수를 뜻합니다. functonal programming 에서는 함수 또한 값으로 여겨집니다. 그렇기 때문에 함수의 인자값과 리턴값이 함수가 될수 있는것을 Higher-order Function 이라고 합니다.

Pure Function

pure method는 입력에 의해서만 출력이 결정되지만 nonPure methodoper 라는 외부 변수에 의해서 출력결과가 달라집니다. pure method가 Pure Function이라고 할수 있습니다.

int oper = 2;
public int pure(int num){
    return num * num;
}

public int nonPure(int num){
    return num * num * oper;
}

Annonymous Function

lamda를 사용해 함수 이름없이 사용할수 있습니다.

(int a, int b) -> return a * b;

Higher-order Function

map 함수의 parameter는 함수입니다. 이러한 특징이 Higher-order Function입니다.

Stream.of(1,2,3)
        .map((int a) -> return a + 1);

Functional Interface

Java 에서는 functional programming 을 구현하기 위해 Functional Inteface 개념을 도입하였습니다.

  • 선언 method 가 하나인 Interface
  • @FunctionalInterface
@FunctionalInterface
public interface Function<T, R> {

     R apply(T t);

}

Java의 대표적인 @FunctionalInterface입니다. lamda 표현식이 사용가능한 interface 입니다. 참고로 default method는 여러개가 있어도 상관없습니다. @FunctionalInterface는 명시적인 것으로 이 어노테이션이 없어도 FunctionalInterface 조건을 만족한다면 lamda 표현식 사용이 가능합니다.

어노테이션을 추가해 놓고 abstract method가 2개 이상이 될시에는 컴파일 에러가 발생합니다. FunctionalInterface를 구현할 일이 있다면 어노테이션 사용이 유용해 보입니다.

FunctionInterface api

Java8에서 제공하는 몇가지 FunctionalInterface api를 알아보겠습니다.

Functoin<T, R>

Function api 를 이용해서 입력받은 Integer 값을 String type으로 변환해 보겠습니다.

static class FunctionImpl implements Function<Integer, String>{

	@Override
	public String apply(Integer intValue) {
		return String.valueOf(intValue);
	}

		
}
Function<Integer, String> f = FunctionImpl();

String result = f.apply(10);

Function<Integer, String> f2 = new Function<Integer, String>(){

	@Override
	public String apply(Integer t) {
		return String.valueOf(t);
	}
		
};
String result = f2.apply(100);

Function<Integer, String> f3 = (Integer i) -> {
	return String.valueOf(i);
};
String result = f3.apply(100);
Function<Integer, String> f4 = i -> String.valueOf(i);
String result = f4.apply(100);

클래스 참조, 익명함수 사용 방법보다 lamda가 얼마나 간결한지 눈에 확연히 들어옵니다.

BiFunctoin<T, U, R>

T, U 타입을 입력받아 R 타입으로 변환해주는 functional interface입니다. 간단히 2 개의 Long타입을 입력받고 더한 합을 String type 으로 변환해보겠습니다.

BiFunction<Long, Long, String> bf = (v1, v2) -> "[" + (v1 + v2) + "]";
String result = bf.apply(10L , 30L);
기타

Function 이외에도 Predicate, Consumer, BiConsumer, Supplier 등의 FunctionalInterface가 존재합니다.

@FunctionalInterface
public interface Predicate<T> {
	boolean test(T t);
}
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
}
@FunctionalInterface
public interface Supplier<T> {
    T get();
}
활용

lamda 표현식을 stream api로 활용해 보겠습니다.

Stream api
  • Stream 생성
  • 중개 연산 (filter, map, flatmap …)
  • 종단 연산 (reduct, collect, forEach …)

Stream api의 구조는 3단계로 구성합니다.

IntStream.of(1, 3, 5, 4, 10, 16, 2)
				.filter(s -> s > 4)
				.sum();

Stream api에서 filter method는 IntPredicate FuncctionalInterface 를 parameter 로 표현합니다.
lamda 식을 쓰지 않는다면 아래와 같이 표현할수 있습니다.

IntStream.of(1, 3, 5, 4, 10, 16, 2)
		.filter(new IntPredicate() {
			
			@Override
			public boolean test(int value) {
						
				boolean result = false;
				if(value > 4){
					result = true;
				}
							
				return result;
			}
		})
		.sum()

정리

함수형 프로그래밍은 개발 생산성과 재사용성, 테스트, 간결한 분석등 여러가지 장점이 있다고 합니다. 이러한 장점은 둘째치고서라도 일단 코드의 양이 줄어 간결해진것만으로도 큰 장점이라고 생각합니다.
시간이 된다면 scala, haskell 함수형 언어에 대해서 공부해보고 포스팅 해보겠습니다.


참조