본문 바로가기
Web

[Java] 람다식과 함수형 인터페이스의 개념

by DuncanKim 2022. 9. 22.
728x90

[Java] 람다식과 함수형 인터페이스의 개념

 

 

스프링 독서 스터디의 부록으로 java 8부터 등장한 람다식과 함수형 인터페이스 내용을 보면서 정리한 내용이다. 이 두 가지를 왜 알고 있어야 하는지, 어디에, 어떻게 쓸 수 있는지를 알아보고자 한다.

 

 

1. 함수형 인터페이스, 람다식의 등장 이유

 

람다식을 알아보기 전에 먼저 함수형 인터페이스가 왜 나오게 되었는지를 알아보자. 

 

 

1) 옛날보다 많은 기술의 발전

 

세상이 발전해 나가면서 싱글 코어 CPU만 쓰다가 멀티 코어 CPU를 쓰기 시작했다. 컴퓨터의 하드웨어가 많이 발전하였다.  그리고 사람들이 일상에서 활용하는 IT 기술이 발전하면서 의미 있는 데이터들이 생산되기도 했는데, 이에 따라 다량의 데이터를 처리하는 프로그램이 필요했다.

 

이 두 가지 흐름이 1950년대에 언급되었던 함수형 프로그래밍을 실무에서 쓰도록 만들었다. 하나의 CPU 안에 다수의 코어를 삽입하는 멀티 코어 프로세서들이 등장하면서 병렬화 프로그래밍에 대한 필요성이 생긴 것이다. 이 추세를 위해 자바 8에서는 병렬화를 위해 컬렉션(List, Map, Set, Array)을 강화했고, 스트림을 효율적으로 사용하기 위해 함수형 프로그래밍이, 함수형 프로그래밍을 위해 람다가 생겨난 것이다. 여기에 람다를 위해 인터페이스가 변화되었다. 람다를 지원하기 위한 인터페이스를 함수형 인터페이스라고 한다. 

 

2) 결론

 

CPU의 발달, 그리고 빅데이터 처리를 자바에서도 할 수 있도록 하려고 람다식과 함수형 인터페이스가 도입된 것이라 볼 수 있다.

 

 

 

2. 람다식의 기본 문법

 

1) 매개변수가 있는 타입(리턴값이 없을 때)

 

(타입 매개변수, ...) -> {실행문 ....; };

ex)
// 예시
(int a) -> {System.out.println(a);};

/*
람다식은 런타임 시에 대입되는 값에 따라 매개변수 타입을 
자동으로 인식함. 따라서 타입을 일반적으로 쓰지 않음.
*/

// 일반적인 작성방법
(a) -> {System.out.println(a);};

/*
매개변수가 하나라면, ()를 생략 가능함.
실행문도 하나라면 {}를 생략할 수 있음.
*/

a -> System.out.pringln(a);

타입 매개변수는 실행문을 실행하기 위해 필요한 값을 제공하는 역할을 한다.

 

 

2) 매개변수가 있는 타입(리턴값이 있을 때)

 

// 기본형
(x, y) -> {return x + y;};

// 중괄호에 return문만 있을 때, 중괄호, return 생략 가능.
(x, y) -> x + y;

 

 

3) 매개변수가 없는 타입

 

() -> {실행문 .... ;};

 

 

3. 타겟 타입과 함수형 인터페이스

 

자바는 메서드를 단독으로 선언할 수 없고 항상 클래스의 구성 멤버로 선언한다. 그래서 람다식은 단순히 메서드를 선언하는 것이 아니라 이 메서드를 가지고 있는 객체를 생성해 낸다. 어떤 타입의 객체를 생성하는 것일까?

 

인터페이스 변수 = 람다식;

 

람다식은 인터페이스 변수에 대입된다. 즉 람다식은 인터페이스 익명 구현 객체를 생성한다는 뜻이다. 인터페이스는 직접 객체로 될 수 없기 때문에 구현 클래스가 필요한데, 람다식은 익명 구현 클래스를 생성하고 객체화한다. 람다식은 대입될 인터페이스의 종류에 따라 작성 방법이 달라지기 때문에 람다식이 대입될 인터페이스를 람다식의 타겟 타입이라고 한다.

 

 

1) 함수형 인터페이스(@FunctionalInterface)

 

람다식은 하나의 메서드만 정의한다. 두 개 이상의 추상 메서드가 선언된 인터페이스는 람다식을 이용하여 구현 객체를 생성할 수 없다. 하나의 추상 메서드가 선언된 인터페이스만 람다식의 타겟 타입이 될 수 있는데, 이런 인터페이스를 함수형 인터페이스라고 한다.

 

함수형 인터페이스를 작성할 때 두 개 이상의 추상메서드가 선언되지 않도록 컴파일러가 체킹 해주는 기능이 있는데, @FunctionalInterface 어노테이션을 붙이면 된다. 만약 이것을 붙이고 두 개 이상의 추상 메서드가 선언되면 컴파일 오류를 발생시킨다. 이것은 필수 사항은 아니고, 선택사항일 뿐이다. 실수를 체킹 하고 싶다면 붙이는 것이 좋을 것 같다.

 

 

2) 매개변수와 리턴값이 없는 람다식

 

이러한 함수형 인터페이스가 있다고 가정해보자.

@FunctionalInterface
public interface MyFunctionalInterface {
	public void method();
}

 

이 인터페이스를 타겟타입으로 가지려면 람다식은 아래와 같이 작성하면 된다.

 

MyFunctionalInterface fi = () -> { System.out.println("hi"); };


// 호출시
fi.method();
// 출력 : hi

fi = () -> { System.out.println("bye"); };

fi.method();
// 출력 : bye

 

method가 매개 변수를 가지지 않기 때문에 람다식도 마찬가지로 매개변수를 가지지 않는다. 이 식이 호출된다면 hi 가 출력될 것이다.

이렇게 여러 번 정의를 하여 내부의 실행문을 바꿔서 함수를 호출할 수도 있다.

 

 

3) 매개변수가 있고 리턴 값이 없는 람다식

 

@FunctionalInterface
public interface MyFunctionalInterface {
	public void method(int x);
}

매개변수가 있는 추상 메서드의 경우 아래와 같이 사용할 수 있다.

 

MyFunctionalInterface fi = (x) -> { System.out.println(x * 5); };


// 호출시
fi.method(5);
// 출력 : 25

fi = (x) -> { System.out.println(x * 6); };

fi.method(5);
// 출력 : 30

이렇게 사용할 수 있다.

 

4) 매개변수와 리턴값이 있는 람다식

 

@FunctionalInterface
public interface MyFunctionalInterface {
	public void method(int x, int y);
}

두 개를 인자로 받는 추상 메서드가 있다고 하자.

 

MyFunctionalInterface fi = (x, y) -> { return x + y; };


// 호출시
fi.method(5, 2);
// 출력 : 7

fi = (x, y) -> x - y;

fi.method(5, 2);
// 출력 : 3

이렇게 사용할 수 있다.

 

 

5) 클래스 멤버와 로컬 변수를 사용한 람다식

 

(1) 클래스 멤버를 사용하는 람다식

 

this를 활용하여 필드와 메서드를 제약사항 없이 사용할 수 있다. 다만, 일반적으로 익명 객체 내부에서 this는 익명 객체의 참조이지만, 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조라는 것을 주의해야 한다.

 

public interface MyFunctionalInterface {
    public void method();
}

 

public class UsingThis {
	public int outterField = 10;

	class Inner {
		int innerField = 20;

		void method() {
			//람다식
			MyFunctionalInterface fi= () -> {
				System.out.println("outterField: " + outterField);
				System.out.println("outterField: " + UsingThis.this.outterField + "\n");
				
				System.out.println("innerField: " + innerField);
				System.out.println("innerField: " + this.innerField + "\n");
			};
			fi.method();
		}
	}
}

바깥 객체의 참조를 얻기 위해서는 클래스명. this를 사용해야 한다.

람다식 내부에서 this는 Inner의 객체를 참조한다. 즉 20을 참조하는 것이다.

 

 

public class UsingThisExample {
	public static void main(String... args) {
		UsingThis usingThis = new UsingThis();
		UsingThis.Inner inner = usingThis.new Inner();
		inner.method();
	}
}

10, 20이 차례로 출력될 것이다.

 

 

(2) 로컬 변수를 사용하는 람다식

 

매개 변수 또는 로컬 변수를 람다식에서 읽는 것은 허용되지만, 람다식 내부 또는 외부에서 변경할 수 없다는 점을 주의해야 한다. 지역 변수와 전역 변수의 차이, 그리고 익명 객체의 로컬 변수 사용의 특징을 생각해보면 이해할 수 있을 것이다.

 

public interface MyFunctionalInterface {
    public void method();
}

 

public class UsingLocalVariable {
	void method(int arg) {  //arg는 final의 특성을 가진다.
		int localVar = 40; 	//localVar는 final 특성을 가진다.
		
		//arg = 31;  		//사용 불가. final 특성 때문에 수정이 불가능
		//localVar = 41; 	//사용 불가. final 특성 때문에 수정이 불가능
        
		//람다식
		MyFunctionalInterface fi= () -> {
			//로컬변수 읽기
			System.out.println("arg: " + arg); 
			System.out.println("localVar: " + localVar + "\n");
		};
		fi.method();
	}
}

 

public class UsingLocalVariableExample {
	public static void main(String... args) {
		UsingLocalVariable ulv = new UsingLocalVariable();
		ulv.method(20);
	}
}

20, 40이 차례대로 출력이 될 것이다.

 

 

4. 표준 API의 함수형 인터페이스

위에서는 직접 함수형 인터페이스를 만들어서 람다식을 사용해보았다.

 

자바는 이런 식의 표준 API를 제공한다. java.util.function 표준 API 패키지는 메서드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 한다. java.util.function 패키지의 함수형 인터페이스는 Consumer, Supplier, Function, Operator, Predicate로 구분된다. 구분 기준은 인터페이스에 선언된 추상 메서드의 매개값과 리턴 값의 유무이다.

 

종류 추상 메서드 특징
Consumer - 매개값은 있고, 리턴값은 없음
Supplier - 매개값은 없고, 리턴값은 있음
Function - 매개값도 있고, 리턴값도 있음
- 주로 매개값을 리턴값으로 매핑함(타입 변환)
Operator - 매개값도 있고, 리턴값도 있음
- 주로 매개값을 연산하고 결과를 리턴함.
Predicate - 매개값은 있고, 리턴 타입은 boolean 타입
- 매개값을 조사해서 true/false를 리턴하는 것임.

 

 

1) Consumer 함수형 인터페이스

 

(1) 인터페이스 메서드

인터페이스 명 추상 메서드 설명
Consumer void accept(T t) 객체 T를 받아 소비
BiConsumer<T, U> void accept(T t, U u) 객체 T와 U를 받아 소비
IntConsumer void accept(int value) int 값을 받아 소비
LongConsumer void accept(long value) long 값을 받아 소비
DoubleConsumer void accept(double value) double 값을 받아 소비
ObjIntConsumer void accept(T t, int value) 객체 T와 int 값을 받아 소비
ObjLongConsumer void accept(T t, long value) 객체 T와 long 값을 받아 소비
ObjDoubleConsumer void accept(T t, double value) 객체 T와 double 값을 받아 소비

 

소비한다는 말은 사용만 할 뿐 리턴 값이 없다는 것이다.

 

 

(2) 사용 예시

import java.util.function.*;

public class Main {
    public static void main(String[] args) {
        Consumer<String> consumer = (s)-> System.out.println(s);
        // <String>이므로 매개값 s는 String
        consumer.accept("A String.");

        BiConsumer<String, String> biConsumer = 
                         (t,u) -> System.out.println(t+","+u);
        // <String,String> 이므로 매개값 t와 u는 모두 String 타입
        biConsumer.accept("Hello","world");

        // 오토박싱/ 언방식 사용하면 비효율적이다.
        Consumer<Integer> integerConsumer = (x) -> System.out.println(x);
        integerConsumer.accept(10); // 값이 들어갈 땐 오토박싱 출력할 때 언박싱

        // 효율적으로 하기 위해서 IntConsumer 제네릭이 아니다 기본형 타입
        // 기본형 입력을 하려고 할 경우, PConsumber (p: primitive type)을 사용 가능.
        // 주의! 오버로딩이 아니고 별도의 인터페이스이다. 
        // 최적화를 위해서 불편하더라도 별도로 만들어 놓은 것이다.
        IntConsumer intConsumer = (x) -> System.out.println(x);
        intConsumer.accept(5); 
        // 객체가 아니라 값을 입력을 받는 것이다. 기본자료형이니깐
        //LongConsumer, DoubleConsumer

        // t는 <>안에 값 x는 objIntconsumber의 int의 자료형이 들어간다.
        ObjIntConsumer<String> objIntConsumer = 
                       (t,x) -> System.out.println(t + ": "+ x);
        objIntConsumer.accept("x",1024);
        // ObjLongConsumer,ObjDoubleConsumer
        // 총 4가지 타입이 있다.
    }
}

 

 

2) Supplier 함수형 인터페이스

 

(1) 인터페이스 메서드

인터페이스 명 추상 메서드 설명
Supplier T get() T 객체를 리턴
BooleanSupplier boolean getAsBoolean() boolean 값을 리턴
IntSupplier int getAsInt() int 값을 리턴
LongSupplier long getAsLong() long 값을 리턴
DoubleSupplier double getAsDouble() double 값을 리턴

 

(2) 사용 예시

import java.util.function.BooleanSupplier;
import java.util.function.IntSupplier;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Supplier<String> supplier = () -> "A String";
        // 입력을 받지 않기때문에 ()이 필요하다.
  
        System.out.println(supplier.get()); 
        // get()을 해서 출력을 한다.
        // BiSupperlier는 입력은 여러 개 할 수 있지만, 
        // 출력은 하나 밖에 못하기 때문에 없다.

        //Supplier는 P Type 계열에서 getAsP 메소드로 정의가 된다. primitive
        // 메소드가 다르다. getAsInt()...
        BooleanSupplier boolsup = () -> true;
        System.out.println(boolsup.getAsBoolean()); 
        // 이것은 getAsBoolean()으로 출력한다.
        // IntSupplier, LongSupplier, DoubleSupplier

        IntSupplier rollDice = () -> (int)(Math.random() * 6);
        //0~6까지 나와서 6은 나오지 않음 0~5까지만 실제 값이 나온다.
        for (int i = 0; i < 10; i++) {
            System.out.println(rollDice.getAsInt());
        }

        int x = 4;
        IntSupplier intSupp = () -> x; //로컬변수에도 접근할 수 있다.
        // 람다식을 활용할 때 모든 변수에 접근하여 활용할 수 있다.
        // 고정되어있는 값뿐만아니라 동적으로도 주변 값들을 공급할 수 있다.
        // 그래서 supplier가 나름대로의 의미가 있다??
        System.out.println(intSupp.getAsInt());

    }
}

 

 

3) Function 함수형 인터페이스

 

(1) 인터페이스 메서드

 

인터페이스 명 추상 메서드 설명
Function<T, R> R apply(T t) 객체 T를 객체 R로 매핑
BiConsumer<T, U, R> R apply(T t, U u) 객체 T와 U를 객체 R로 매핑
PFunction(하단 참조) R apply(p value) 기본자료형 p(하단 참조)를 객체 R로 매핑
PtoQFunction(하단 참조) q applyAsQ(p value) 기본자료형 p를 기본자료형 q로 매핑
ToPFunction(하단 참조) p applyAsP(T t) 객체 T를 기본자료형 p로 매핑
ToPBiFunction<T, U>(하단 참조) p applyAsP(T t, U u) 객체 T와 U를 기본자료형 p로 매핑

 

 

  • P, Q는 기본 자료형(Primitive Type) : Int, Long, Double
  • p, q는 기본 자료형(Primitive Type) : int, long, double
  • Funtion<Student, String> funtion = t-> {return t.getName()};
    • <Student,String>이므로 매개값 t는 Student 타입이고 리턴 값은String 타입이다.
    • Student 객체를 String으로 매핑한 예제
  • ToIntFuntion funtion = t -> {return t.getScore();}
    • 이므로 매개값 t는 Student 타입이고 리턴값은 int 타입 고정
    • Student 객체를 int로 매핑한 예제

 

import java.util.function.*;

public class Main {
    public static void main(String[] args) {
        Function<String,Integer> func = (s) -> s.length();
        // s 는 String타입, s.length() 는 Integer
        System.out.println(func.apply("Strings")); //이것은 apply로 출력한다

        // Bi가 붙으면 '입력'을 2개 받을 수 있다는 의미이다.
        BiFunction<String,String,Integer> biFunction = (s,u) -> s.length() + u.length();
        System.out.println(biFunction.apply("one","two")); //6

        // IntFunction<R>은 리턴 자료형
        // P type Funtion은 입력을 P타입으로 받는다.
        IntFunction<String> intFunction = (value) -> String.valueOf(value);// "" + value도 가능.
        System.out.println(intFunction.apply(512));

        //ToP Type Function은 출력을 P타입으로 한다.
        ToIntFunction<String> funcFour = (s) -> s.length(); // 4:21
        System.out.println(funcFour.applyAsInt("abcde"));
        // 출력이 P타입인 경우에는 AsP가 들어간다.!!!
        //ToIntBiFunction<String,String>// int 출력을 하는 Bi 함수
        // P: Int, Long, Double

        // int 에서 double로 바꾸는 함수 PToQFunction : P -> Q로 매핑하는 함수
        IntToDoubleFunction funcfive;
        // IntToIntFunction은 없다. 동일한 것에 대해서는 다른게 있다.
    }
}

 

 

 

4) Operator 함수형 인터페이스

 

(1) 인터페이스 메서드

인터페이스 명 추상 메서드 설명
UnaryOperator T apply(T t) T를 연산한 후 T 리턴
BinaryOperator T apply(T t1, T t2) T와 T를 연산한 후 T 리턴
IntUnaryOperator int applyAsInt(int value) 한 개의 int 연산
LongUnaryOperator long applyAsLong(long value) 한 개의 long 연산
DoubleUnaryOperator double applyAsDouble(double value) 한 개의 double 연산
IntBinaryOperator int applyAsInt(int value1, int value2) 두 개의 int 연산
LongBinaryOperator long applyAsLong(long value, long value2) 두 개의 long 연산
DoubleBinaryOperator double applyAsDouble(double value, double value2) 두 개의 double 연산

 

 

import java.util.function.BinaryOperator;
import java.util.function.IntBinaryOperator;
import java.util.function.IntUnaryOperator;
import java.util.function.UnaryOperator;

public class Main {
    public static void main(String[] args) {
        // 그냥 operator는 없다.
        // 입력이 1개 인 것을 Unary를 붙여서 표현
        UnaryOperator<String> operator = s -> s+"."; 
        // 리턴타입을 따로 입력받지 않는다 입출력이 같으니깐
        System.out.println(operator.apply("왔다")); // apply() 사용.

        BinaryOperator<String> operator1 = (s1,s2) -> s1 + s2;
        // 타입은 하나만 입력받게 되어있다. 출력은 동일한 타입이여야 하니깐?
        System.out.println(operator1.apply("나","왔다"));

        IntUnaryOperator op = value -> value*10; 
        //타입을 받지 않는다 어차피 int입력 int출력이니
        System.out.println(op.applyAsInt(5));
        // LongUnaryOperator, DoubleUnaryOperator

        IntBinaryOperator ibo = (v1,v2) -> v1 * v2;
        System.out.println(ibo.applyAsInt(10,20));
        //LongBinaryOperator, DoubleBinaryOperator
    }
}

 

 

 

5) Predicate 함수형 인터페이스

 

 

(1) 인터페이스 메서드

 

인터페이스 명 추상 메서드 설명
Predicate boolean test(T t) 객체 T를 조사한다.
BiPredicate<T, U> boolean test(T t, U u) 객체 T와 U를 비교하여 조사한다.
IntPredicate boolean test(int value) int 값을 조사한다.
LongPredicate boolean test(long value) long 값을 조사한다.
DoublePredicate boolean test(double value) double 값을 조사한다.

 

 

(2) 사용 예시

 

import java.util.function.BiPredicate;
import java.util.function.IntPredicate;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<String> predicate = (s) -> s.length() == 4; 
        // 조건식이 들어가야 한다.
        System.out.println(predicate.test("Four")); 
        // test()를 사용한다 true or false 값 출력
        System.out.println(predicate.test("six"));

        BiPredicate<String, Integer> pred2 = (s,v) -> s.length() ==v;
        System.out.println(pred2.test("abcd",23));
        System.out.println(pred2.test("abc",3));

        IntPredicate pred3 = x -> x > 0;
        //LongPredicate, DoublePredicate asP출력은 존재하지 않는다.

    }
}

 

 

 

 

 

참고

- https://velog.io/@im_joonchul/%ED%91%9C%EC%A4%80-%ED%95%A8%EC%88%98%ED%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4
- 신용권, <이것이 자바다>
728x90

댓글