무작정 개발.Vlog

[Java8] 함수형 인터페이스, 람다 표현식

by 무작정 개발
반응형

이번에는 Java의 함수형 터페이스(Functional Interface)람다 표현식(Lambda Expression)에 대해 정리할 것이다.

 

1. 함수형 인터페이스

함수형 인터페이스(Functional Interface)는 추상 메서드(Abstract Method)를 딱 1개만 가지고 있는 인터페이스 혹은

@Functionalinterface 어노테이션을 붙인 인터페이스를 말합니다. 혹은 SAM(Single Abstract Method)라고도 부릅니다.

public interface RunSomething {

    abstract void doIt(); // -> abstract 키워드는 생략 가능
}

 

추가로 Java8 버전부터는 인터페이스에 스태틱 메서드(static method), 기본 메서드(default method)를 정의할 수 있습니다.

핵심은 static 메서드와 default 메서드가 인터페이스 안에 존재하더라도 추상 메서드가 1개만 존재하면 함수형 인터페이스입니다.

 

public interface RunSomething {

		abstract void doIt();
        
        // public 생략 가능
        static void print() {
        		System.out.println("static method");
        }
        
        // public 생략 가능
        default void print() {
        		System.out.println("default method");
        }
}

 

[ Java에서의 함수형 프로그래밍 ]

  • 함수를 First Class Object로 사용할 수 있다.
  • 순수 함수 (Pure function)
    • 사이드 이펙트를 만들 수 없다. (함수 밖에 있는 값을 변경하지 못함)
    • 상태가 없다. (함수 밖에 정의되어 있는)
  • 고차 함수 (High-Order Function)
    • 함수가 함수를 매개 변수로 받을 수 있고 함수를 리턴할 수 있다.
  • 불변성 (Immutability)

2. Java 8에서 제공하는 함수형 인터페이스

 

public interface RunSomething {
	void doIt(); // abstract 키워드 생략 가능 
}

함수형 인터페이스를 직접 정의하지 않아도 Java에서 기본으로 제공하는 함수형 인터페이스가 있습니다.

Java에서 제 공하는 함수형 인터페이스는 [ java.util.function ] 패키지에 포함되어 있습니다.

 

java.util.function (Java Platform SE 8 )

Interface Summary  Interface Description BiConsumer Represents an operation that accepts two input arguments and returns no result. BiFunction Represents a function that accepts two arguments and produces a result. BinaryOperator Represents an operation u

docs.oracle.com

 

[Java에서 미리 정의해둔 자주 사용할 만한 함수형 인터페이스]

  • Function<T, R>
  • BiFunction<T, U, R>
  • Consumer<T>
  • Supplier<T>
  • Predicate<T>
  • UnaryOperator<T>
  • BinaryOperator<T>

 

(1) Function<T, R>

T 타입을 받아서 R 타입으로 반환하는 함수 인터페이스

  • R apply (T t)
    • T라는 타입을 받아서 R이라는 타입으로 반환하는 추상 메서드(abstract method)

 

[ Function<T,R> 구현체 클래스를 이용한 방법 ]

public class Plus10 implements Function<Integer, Integer> {
    @Override
    public Integer apply(Integer integer) {
        return integer + 10;
    }
}

// ======================================================================

public class Test {
    public static void main(String[] args) {
        Plus10 plus10 = new Plus10();
        System.out.println(plus10.apply(1)); //11 -> 1 + 10
    }
}

1. Function 인터페이스를 구현하는 Plus10 클래스를 생성해서 추상 메서드 apply()를 구현

2. Plus10의 인스턴스(객체)를 생성 후 apply(1)을 호출해서 결괏값을 반환받는다.

 

 

[ 위의 코드를 람다 표현식(Lambda Expression)을 이용한 방법으로 구현 ]

public class Test {
    public static void main(String[] args) {
    	// Lambda Expression 사용
        Function<Integer, Integer> plus10 = (i) -> i + 10;
        System.out.println(plus10.apply(1)); //11 -> 1 + 10
    }
}

Function<Integer, Integer> plus10 = (i) -> i + 10; -> 이렇게 람다 표현식을 이용해서 구현이 가능합니다.

 

(2) 함수 조합용 메서드 - andThen(), compose()

Function 인터페이스의 default 메서드인 compose(), andThen() 메서드를 사용하면 두 함수형 인터페이스를 조합할 수 있습니다.

 

- andThen()

  • 해당 함수를 호출하는 인스턴스가 먼저 수행된 후 반환 값을 andThen() 함수 파라미터로 넘겨준다.
public class Test {
    public static void main(String[] args) {
    
        Function<Integer, Integer> plus10 = (i) -> i + 10;
        Function<Integer, Integer> multiply2 = (i) -> i * 2;
        System.out.println(plus10.andThen(multiply2).apply(2)); // 24
    }
}

[ 동작 과정 ]

  1. plus10.andThen(multiply2).apply(2)
    • apply(2)로 전달받은 매개 변수 2를 plus10 함수에서 실행 후 andThen의 매개 변수 multiply2에 전달하면
    • ((2 + 10) * 2)가 되어 최종으로 24를 반환

 

- compose()

  • 파라미터에 입력받은 결괏값(함수의 결과)을 함수를 호출한 인스턴스에 전달해 apply() 하는 함수
public class Test {
    public static void main(String[] args) {
    
        Function<Integer, Integer> plus10 = (i) -> i + 10;
        Function<Integer, Integer> multiply2 = (i) -> i * 2;
        Function<Integer, Integer> multiply2AndPlus10 = plus10.compose(multiply2);
        System.out.println(multiply2AndPlus10.apply(2)); // 14
    }
}

[ 동작 과정 ]

  1. Function<Integer, Integer> plus10 = (i) -> i + 10;
    • 매개 변수에 10을 더해 반환하는 순수 함수(pure function)
  2. Function<Integer, Integer> multiply2 = (i) -> i * 2;
    • 매개 변수에 2를 곱해서 반환하는 순수 함수(pure function)
  3. Function<integer, integer=""> multiply2AndPlus10 = plus10.compose(multiply2);
    • plus10 함수와 multiply2 함수를 조합해주는 함수
    • 해당 함수에서는 multiply2가 먼저 실행되어 반환된 값을 plus10에 전달하여 결과적으로 매개 변수에 2를 곱한 뒤 10을 더해주는 조합 함수
  4. multiply2AndPlus10.apply(2)

 

(3) BiFunction<T, U, R>

Function<T, R>과 동일하지만 U라는 타입을 1개 더 받는다. 그래서 T, U 타입을 받아 R을 반환하는 함수형 인터페이스이다.

  • R apply(T t, U u)
public class Test {
    public static void main(String[] args) {
    
        BiFunction<Integer, String, String> prefixHello = (i, s) ->{
            return "hello"+i.toString() + s;
        };
        
        System.out.println(prefixHello.apply(10, "catsbi"));//hello10catsbi
    }
}

[ 동작 과정 ]

  1. Bifunction<Integer, String, String> prefixHello = (i, s) -> { .... };
    • 매개 변수가 1개 더 늘어났다는 점을 제외하면 Function<T, R> 함수형 인터페이스와 동일합니다.

 

(4) Consumer<T>

T 타입을 받아서 아무 값도 리턴하지 않는(반환 값이 없음) 함수형 인터페이스

  • accept(T t)
  • andThen() : 함수 조합용 메서드
public class Test {
    public static void main(String[] args) {
    
        Consumer<Integer> printT = (i) -> System.out.println(i);
        printT.accept(10);
    }
}

 

(5) Supplier<T>

T 타입의 값을 제공하는 함수형 인터페이스

  • T get()
public class Test {
    public static void main(String[] args) {
    
        Supplier<Integer> get10 = () -> 10;
        System.out.println(get10); // 10
    }
}

[ 동작 과정 ]

  1. Supplier<Integer> get10 = () -> 10;
    • 입력하는 값이 없기 때문에 매개 변수가 없고, T 타입의 값을 반환만 해준다.

 

(6) Predicate<T>

T 타입을 받아서 boolean을 리턴하는 함수형 인터페이스

  • boolean test(T t)
  • and, or, negate (함수 조합용 메서드)
public class Test {
    public static void main(String[] args) {

        Predicate<Integer> isOdd = (i) -> i%2 == 0;
        Predicate<Integer> isEven = (i) -> i%2 == 1;

        System.out.println(isOdd.and(isEven).test(4));
        System.out.println(isOdd.or(isEven).test(4));
        System.out.println(isOdd.negate().test(3));
    }
}

 

(7) UnaryOperator<T>

Function<T, R>의 특수한 형태로 입력값 1개를 받아 동일한 타입을 리턴하는 함수형 인터페이스

 

public class Test {
    public static void main(String[] args) {
    
        UnaryOperator<Integer> plus10 = (i) -> i + 10;
        UnaryOperator<Integer> multiply2 = (i) -> i * 2;
        System.out.println(plus10.andThen(multiply2).apply(2));
    }
}

[ 동작 과정 ]

  1. UnaryOperator<Integer> plus10 = (i) -> i * 10;
    • 기존 Function<T, R>과 비슷하지만 다른 점은 반환 타입 R을 따로 작성하지 않는다.
    • 왜냐하면 T 타입이 반환 타입이 되기 때문이다.
    • UnaryOperator<T>는 입력 타입과 반환 타입이 동일하다.

 

(8) BinaryOperator<T>

BiFunction<T, U, R>의 특수한 형태로, 동일한 타입의 입력값 2개를 받아 리턴하는 함수형 인터페이스

 

public class Test {
    public static void main(String[] args) {
    
        BinaryOperator<Integer> plus10Combine = (i, j) -> i + j + 10;
        System.out.println(plus10Combine.apply(10, 20));//40
    }
}

 


3. 람다 표현식 (Lambda Expression)

 

 

Java8 버전 이전에는 인터페이스를 구현하는 방법은 구현 클래스를 통한 방법, 익명(Anonymous) 구현 객체를 통한 구현이 대표적이었습니다.

 

[ Anonymous Class 구현]

public class AnonymousToLambda {

	public static void main(String[] args) {
    
    		// Java 8 이전의 방식 ( 익명 구현 객체[익명 클래스] ) 
		RunSomething rs = new RunSomething() {
        
			@Override
			public void doIt() {
				System.out.println("Anonymous Class!");
			}
		};
		rs.doIt();
	}
}

 

하지만 Java8 버전부터는 추상 메서드를 1개만 가지고 있는 함수형 인터페이스를 람다 표현식으로 작성할 수 있습니다.

상단의 코드와 하단의 코드는 모두 동일한 코드로, 람다 표현식을 사용하면 코드를 줄이고 가독성을 높일 수 있습니다.

 

[ Lambda Expression ]

public class AnonymousToLambda {

	public static void main(String[] args) {
    
		// 람다 표현식(Lambda Expression)
		RunSomething rs = () -> System.out.println("Anonymous Class!");
		
		rs.doIt();
	}
}

 

Java에서 람다 표현식(Lambda Expression)은 함수형 인터페이스를 인라인(InLine)으로 구현한 특수한 형태의 객체이기 때문에

레퍼런스에 할당이 가능하고, 메서드의 파라미터로 전달할 수 있고, 메서드의 반환 값으로 넘길 수 있습니다.

 

 

[ 람다 표현식 ]

  • 함수형 인터페이스의 인스턴스를 만드는 방법으로 사용 가능하다.
  • 코드를 줄이고, 가독성을 높일 수 있다.
  • 메서드 매개 변수, 리턴 타입, 변수로 만들어 사용 가능하다.

4. 람다 표현식과 변수 캡처

 

(1) 람다(Lambda)

 

(인자 리스트) -> { body }
  • 인자 리스트
    • 인자가 없을 때 : ( )
    • 인자가 1개일 때 : (one) 또는 one으로 괄호() 생략 가능
    • 인자가 여러 개일 때 : (one, two) 괄호 생략 X
    • 인자의 타입(Type)은 생략 가능 / 컴파일러가 추론(infer)하지만 명시할 수도 있다. (Integer one, Integer two)
  • Body(바디)
    • 화살표(->) 오른쪽에 함수 본문을 정의
    • 여러 줄인 경우 { }를 사용해서 묶는다.
    • 1 줄인 경우 { }를 생략 가능하며, return 또한 생략 가능

 

// 생략 전
Function<Integer, Integer> plus10 = (input) -> {
    return input + 10;
};

// 괄호{ } , return 생략
Function<Integer, Integer> plus20 = (input) -> input + 20; // 괄호, return 생략 가능

 

(2) 변수 캡처(Variable Capture)

private void run() {

    int baseNumber = 10; //effective final variable
		//final int baseNumber = 10; //Java 8 부터 final 생략 가능

    // 로컬 클래스
    class LocalClass {
        void printBaseNumber(){
            System.out.println(baseNumber);
        }
    }
    
    // 익명 클래스
    Consumer<Integer> integerConsumer = new Consumer<Integer>() {
        @Override
        public void accept(Integer integer) {
            System.out.println(baseNumber);
        }
    };

    // 람다 표현식
    IntConsumer printInt = (i) -> System.out.println(i + baseNumber);
    printInt.accept(10);

}

 

로컬 클래스, 익명 클래스, 람다 모두 baseNumber이라는 지역 변수(Local 변수)를 참조할 수 있습니다.

여기서 참조 가능한 로컬 변수는 final이거나 effective final인 경우에만 참조할 수 있고, 그렇지 않을 경우 concurrency 문제가 생길 수 있어 컴파일 에러가 발생합니다.

 

추가로 람다(Lambda)의 Body에서 지역 변수(Local 변수)를 사용하게 되면 지역 변수가 [ 캡처 ] 됩니다.

변수 캡처라는 개념은 Java8 이전에 익명 클래스(Anonymous Class)와 내부 클래스에서도 쓰이던 기능으로 Java 8 이전에는

변수 캡처를 위해선 final 키워드를 붙여야 했습니다.

 

Java 8부터는 해당 지역 변수(Local 변수)가 "사실상 final"일 때 final 키워드를 생략할 수 있고 이를 effective final이라 부릅니다.

 

[ 사실상 final ]
final 키워드는 없지만 변수를 어디서도 변경하지 않을 때 해당 변수를 사실상 final 혹은 effective final이라 한다.

 

(3) 쉐도잉(Shadowing)

여기까지만 보면 Lambda가 굳이 차이점이 없어 보이지만 로컬 클래스, 익명 클래스와 가장 큰 차이점은 쉐도잉에 있습니다.

 

[ Shadowing 그림 예시 ]

Shadowing
Shadowing

 

[ Shadowing 코드 예시 ]

public class Capture {
 
	private int number = 10;
	
	public static void main(String[] args) {
		Capture c = new Capture();
		c.shadowing();
	}
	
	private void shadowing() {
		int number = 20;
		
		System.out.println(number); // 20
	}
}

먼저 쉐도잉을 상단 코드로 예시를 들자면, Capture 클래스 안에 인스턴스 변수(전역 변수) number가 있고,

shadowing() 메서드 안에 지역 변수(로컬 변수) number이 있을 때, shadowing() 메서드 내부에서 인스턴스 변수(전역 변수) number가 가려지는 현상을 쉐도잉(Shadowing)이라고 합니다.

 

 

다시 익명 클래스, 내부 클래스, 람다로 돌아와서,

익명 클래스, 내부 클래스는 shadowing이 가능하지만 람다 표현식은 shadowing이 불가능합니다.

 

[ Why? ]

익명 클래스, 내부 클래스의 선언부가 그 자체로 별도의 스코프(scope)를 가지고 있기 때문입니다.

반면 람다 표현식은 람다를 감싸고 있는 메서드와 동일한 스코프를 가지고 있기에 shadowing이 불가능합니다.

 

즉, 람다는 표현식 내에 같은 이름의 변수를 선언할 수 없습니다. (같은 스코프이기 때문에!)

 


5. 메서드 레퍼런스

(1) 개요 - 메서드 레퍼런스란?

 

기존에 람다 표현식(Lambda Expression)은 직접 인라인(InLine)으로 기능들을 구현했습니다.

하지만, 매번 단순한 기능부터 복잡한 기능까지 새로 구현하는 것은 매우 비효율적입니다.

 

람다(Lambda)를 통해 이미 존재하는 메서드를 호출하거나 생성자 호출, 혹은 인스턴스의 메서드를 호출하는 거라면,

메서드 레퍼런스를 사용해서 매우 간결하게 표현할 수 있습니다.

 

[ 메서드 참조 방식 ]

 

참조하려는 메서드의 형태에 따라 그 방식이 서로 다릅니다.

  • 스태틱 메서드(static method) 참조
    • 타입::스태틱 메서드
  • 특정 객체의 인스턴스 메서드 참조
    • 객체 레퍼런스::인스턴스 메서드
  • 임의 객체의 인스턴스 메서드 참조
    • 타입::인스턴스 메서드
  • 생성자 참조
    • 타입::new

 

(3) 메서드 레퍼런스 예제

 

[ 먼저 예제에서 사용할 클래스 ]

public class Greeting {

    private String name;
	
    // 인자가 없는 생성자
    public Gretting() {
    }
	
    // name을 인자로 받는 생성자
    public Greeting(String name) {
        this.name = name;
    }
	
 	// name을 가져오는 getName() 메서드
    public String getName() {
        return name;
    }
	
    // name을 인자로 받는 hello() 메서드
    public String hello(String name) {
        return "hello " + name;
    }
	
    // name을 인자로 받고 "hi"를 붙여서 반환하는 hi() 메서드
    public static String hi(String name) {
        return "hi " + name;
    }
}

 

  • 기존 방식으로  기능 구현
// 기존 방식으로 기능 구현
UnaryOperator<String> hi = (s) -> "hi " + s;
System.out.println(hi.apply("HyunWoo")); // hi HyunWoo

기존의 Lambda를 이용한 방법이라면 hi라는 메서드를 직접 만들어 문자열을 결합 후 반환합니다.

하지만, 이 로직은 이미 Greeting라는 클래스에 구현되어 있습니다.

 

  • 스태틱 메서드 참조하여 메서드 레퍼런스로 사용
// 메서드 레퍼런스 - 스태틱 메서드 참조
UnaryOperator<String> hi = Greeting::hi;
System.out.println(hi.apply("HyunWoo")); // hi HyunWoo

Greeting클래스에 구현되어있는 static 메서드 hi()를 메서드 레퍼런스(static 메서드 참조)를 사용하여 호출하였습니다.

 

  • 특정 객체의 인스턴스 메서드를 메서드 레퍼런스로 사용
Greeting greeting = new Greeting();
UnaryOperator<String> hello = greeting::hello;
System.out.println(hello.apply("catsbi")); //hello HyunWoo

Greeting 클래스의 인스턴스(greeting)를 생성 후 해당 인스턴스의 메서드 hello()를 레퍼런스로 이용해 호출하고, 결괏값으로 hello HyunWoo가 출력되었습니다.

 

  • 생성자를 메서드 레퍼런스로 구현
//입력값이 없는 생성자 호출
Supplier<Greeting> newGreeting = Greeting::new;
Greeting greeting = newGreeting.get();

//입력값이 있는 생성자 호출
Function<String, Greeting>  newGreetingWithName = Greeting::new;
Greeting catsbiGreeting = newGreetingWithName.apply("HyunWoo");

파라미터가 없고 반환 값만 있는 기본 생성자는 Supplier를 통해 구현하였습니다.

메서드 레퍼런스로 new 연산자를 사용하는데 주의할 점Greeting::new는 Supplier 인 것이지 실제 인스턴스 생성이 아닙니다. 생성을 위해서는 get() 메서드를 호출하여 인스턴스를 생성해줘야 합니다.

 

파라미터(name)가 있는 생성자는 Function을 통해 구현하였습니다.

생성자 초기값으로 String 인자 값을 받아 전달 후 생성된 Greeting 타입의 인스턴스를 반환합니다.

 

  • 임의의 객체의 인스턴스 메서드 참조

[ names 정렬 구현 ]

String[] names = {"HyunWoo", "Chae", "Test"};

//기존의 익명클래스를 이용한 방법
Arrays.sort(names, new Comparator<String>() {
    @Override
    public int compare(String s, String t1) {
        return 0;
    }
});

//람다 표현식(Lambda Expression)
Arrays.sort(names, (s, t1) -> 0);

//메소드 레퍼런스를 이용한 방법
Arrays.sort(names, String::compareToIgnoreCase);

System.out.println(Arrays.toString(names)); // [HyunWoo, Chae, Test]

Java 8 버전 이전에는 익명 클래스를 이용해 정렬(Sort)을 구현하였습니다.

하지만 Java 8 이후 Comparator 클래스는 @FunctionalInterface가 되었고 함수형 인터페이스이기 때문에 람다 표현식으로 사용할 수 있게 되었습니다.

 

또한, 람다 표현식으로 사용할 수 있다는 것은 메서드 레퍼런스로도 사용 가능한 의미이기에 String 클래스의

campareToIgnoreCase 메서드를 사용하며 names의 파라미터를 넘겨서 정렬(Sort)합니다.


Reference

Kangworld 티스토리 블로그

 

[Java8] Chapter 1-1. 함수형 인터페이스와 람다 표현식

✍️ 함수형 인터페이스 함수형 인터페이스란? 추상 메서드를 딱 하나만 가지고 있는 인터페이스 혹은 @Functionalinterface 어노테이션을 가지고 있는 인터페이스 public interface RunSomething { abstract void

kangworld.tistory.com

인프런-더 자바, Java8_백기선

 

더 자바, Java 8 - 인프런 | 강의

자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합니다. 이

www.inflearn.com

 

반응형

블로그의 정보

무작정 개발

무작정 개발

활동하기