Study 15.람다식

정지은·2023년 2월 27일
0

JAVA를 잡아~

목록 보기
15/15
post-thumbnail

Today's Study


  1. 람다식 사용법
  2. 함수형 인터페이스
  3. Variable Capture
  4. 메소드, 생성자 레퍼런스

1. 람다식 사용법


람다식이란 Java8부터 지원하는 것으로 메소드를 간결한 식으로 표현한 것이다.

익명클래스의 메소드가 하나인 경우, 람다 표현식을 사용해 메소드 인스나 코드를 데이터로 처리하 수 있고 메소드를 직접 정의하지 않아도 간결한 식으로 표현할 수 있다.

람다의 장점

  • 코드의 간결성 : 람다를 사용하면 불필요한 반복문의 삭제가 가능해 복잡한 식을 단순하게 표현할 수 있다.
  • 지연연산 수행 : 람다는 지연연산을 수행할 수 있어 불필요한 연산을 최소화할 수 있다.
  • 병렬처리 가능 : 멀티쓰레드를 활용하여 병렬처리를 사용할 수 있다.

람다의 단점

  • 람다식의 호출이 다소 까다롭다.
  • 람다 stream을 사용시 단순 for문/while문을 사용하게 되면 성능이 떨어질 수 있다.
  • 불필요하게 사용하게 되면 오히려 가독성을 떨어뜨릴 수 있다.

람다의 표현식

() -> {}
() -> 1
() -> { return 1;}

(int x) -> x+1
(x) -> x+1
x -> x+1
(int x) -> { return x+1; }
x -> { return x+1; }

(int x, int y) -> x+y
(x, y) -> x+y
(x, y) -> { return x+y; }

(String lam) -> lam.length()
lam -> lam.length()
(Thread lamT) -> { lamT.start(); }
lamT -> { lamT.start(); }

//잘못된 람다표현
(x, init y) -> x+y
(x, final y) -> x+y

다음을 보다시피

  • 화살표(->)를 사용해 매개변수부와 선언부를 나눈다.
  • 매개변수의 타입을 추론할 수 있을 때는 타입을 생략할 수 있다.
  • 매개변수 작성부에서 변수가 하나인 경우에는 괄호()를 생략할 수 있다.
  • 함수의 몸체가 하나의 명령문만으로 이루어진 경우에는 중괄호{}를 생략할 수 있다. (세미콜론;은 생략)
  • 함수의 몸체에 return문이 있는 경우에는 {}생략 불가
  • return문 대신 표현식을 사용할 수 있으며, 이때의 반환값이 표현식의 결과값이 된다. (세미콜론 =; 생략)

예제

//기존 자바 문법
new Thread(new Runnable() {
	@Override
    public void run(){
    	System.out.println("Hello Java");
    }
}).start();

//람다식 문법
new Thread(()->{
	System.out.println("Hello Java");
}).start();


2. 함수형 인터페이스


함수형 인터페이스는 추상 메소드가 오직 하나인 인터페이스를 말한다. 자바의 람다 표현식은 이러한 함수형 인터페이스로만 사용이 가능하다.

추상메소드가 하나?

추상메소드가 하나라는 말은 default methodstatic method는 여러개 존재해도 된다는 뜻이다. 어노테이션을 공부할 때 @FuncitionalInterface에 대해 배웠었는데 이 어노테이션으로 인터페이스가 함수형 인터페이스인지 검사할 수 있다. ( @FunctionalInterface는 필수는 아니지만 인터페이스의 검증과 유지보수를 위해 사용을 습과화하자.)

함수형 인터페이스&람다 예제

@FunctionalInterface
interface Math{
	public int Calc(int first, int second);
}

public static void main(String[] args){

	Math plus = new Math(){
    	public int Calc(int first, int second){
        	return first+second
        }
    }
    
    //람다로 표현
	Math plus = (first,second) -> first+second;
}

다음과 같이 람다식으로 더 간결히 표현할 수 있다.


자바에서 제공하는 Functional Interfaces

이렇게 매번 함수형 인터페이스를 만들어 사용하는 것은 다소 번거롭다. Java에서는 기본적으로 많이 사용되는 함수형 인터페이스를 제공한다.

함수형 인터페이스DescripterMethod
PredicateT -> booleanboolean test(T t)
ConsumerT -> voidvoid accept(T t)
Supplier() -> TT get()
Function<T,R>T -> RR apply(T t)
Comparator(T,T) -> intint compare(T o1,T o2)}
Runnable() -> voidvoid run()

1) Predicate

public class PredicateTest{
	public static void main(String[] args){
    	Predicate<integer> predicate = (num) -> num > 0;
        boolean result = predicate.test(1);
        System.out.println(result); //true
    }
}

Predicate는 인자하나를 받아 boolean 타입을 리턴합니다. 다음과 같이 num을 받아 num > 0이 맞는지 확인하여 boolean형인 true를 반환합니다.


2) Consumer

public class ConsumerTest{
	public static void main(String[] args){
    	Consumer<Integer> consumer = (num) -> System.out.println(num + 1);
        consumer.accept(1);
    }
}

consumer는 매개변수는 있지만 반환값이 없다. 이름처럼 소비해버리고 말때 사용한다.


3) Supplier

public class SupplierTest{
	public static void main(String[] args){
    	Supplier<String> supplier = () -> "String";
        System.out.println(supplier.get());	//String
    }
}

Supplier는 매개변수없이 파라미터를 반환한다. 정의된 값을 supplier.get()을 통해 반환한다.


4) Function<T,R>

public class FunctionTest{
	public static void main(String[] args){
    	Function<Integer,Intger> function = (num) -> num + 1;
        int result = function.apply(1);
        System.out.println(result);	//2
        
        Function<String, Integer> function2 = String::length;
        int len = function2.apply("Hello World");
        System.out.println(len);
    }
}

T가 데이터 타입, R이 리턴타입으로 매개변수도 있고 반환값도 존재하는 일반적인 적용 함수이다.


5) Comparator

예를 들어 가장 큰수를 찾는 문제를 보자.

public String Max(int[] numbers){
	StringBuffer sb = new StringBuffer();
    
    String[] str = new String[number.length];
    for(int i = 0; i<numbers.length; i++){
    	str[i] = integer.toString(numbers[i]);
    }
    
    //Comparator 인터페이스로 간단히 구현한 부분
    Arrays.sort(str, (a,b) -> {
    	return -(a+b).compareTo(b+a);
    });
    
    if(str[0].equals("0")){
      return "0";
    }
    else {
      for(String s : str) {
        //System.out.println(s);
        sb.append(s);
      }
      return sb.toString();
    }
}

중간의 Arrays.sort부분이 간단히 람다식으로 표현한 부분이다. 원래같으면 아래와 같이 함수 오버라이딩을 통해 구현해야 한다.

Arrays.sort(str, new Comparator<String>() {
          @Override
          public int compare(String o1, String o2) {  
            return -Integer.compare(Integer.parseInt(o1+o2),Integer.parseInt(o2+o1));
            }
        });

6) Runnable

Runnable은 매개변수도, 반환값도 없는 run()메소드를 람다식으로 활용한다.

public class RunnableTest{
	public static void main(String[] args){
    	Runnable runnable = () -> System.out.println("1. Runnable run lambda");
        runnable.run();
        
        Thread t1 = new Thread(runnable);
        t1.start();
        
        Thread t2 = new Thread(() -> System.out.println("2. Runnable run lambda"));
        t2.start();
    }
}

이렇게 Thread객체가 생성되었을 때 파라미터에 들어갈 Runnable 객체전달부분을 람다식으로 정의할 수 있다.

Thread가 start()될때 불러지는 run()을 정의함으로써 로그를 기록하는데 활용할 수 있다.



3. Variable Capture


public class Main{
	private int a = 144;
    
    public void printExample(){
		int b = 222;
        final MyInterface myInterface = () -> System.out.println(a);
    }
}

이 코드에서 람다식을 보면 a는 람다 시그니처의 파라미터로 넘겨진 변수가 아니라 외부에서 정의된 변수로 자유변수(Free Variable) 이다. 이러한 자유 변수를 참조하는 행위를 람다 캡처링(Lambda Capturing) 라고 한다.


이렇게 지역 변수를 람다 캡처링할 때는 제약조건이 있다. - 지역변수가 final로 선언되어 있어야 한다. - final이 아니더라도 지역변수가 final처럼 동작해야 한다.

위의 코드는 a가 final로 작성되어 있지 않았지만 final처럼 동작되고 있기에 가능하다.

public class Main{
	private int a = 144;
    
    public void printExample(){
    	int b = 222;
        a = 1000;
        final MyInterface myInterface = () -> System.out.println(a);
    }
}

하지만 이러한 경우는 값이 변경되어 final도 아니고 final처럼 동작되지도 않는다. 이런식으로 작성되면 안되는 이유가 뭘까?

람다는 Variable Capture를 통해 지역변수를 참조한다.

JVM의 메모리 구조를 보자. JVM에서 지역변수는 스택이라는 영역에 생성된다. JVM에서 스택 영역은 쓰레드마다 별도로 생성된다. 이 말은 쓰레드끼리는 스택을 공유할 수 없다. 는 뜻이다.

람다는 별도의 쓰레드에서 실행이 가능하다. 따라서 원래 지역변수가 있는 쓰레드가 사라져 해당 지역변수가 사라졌다고 하자. 그래도 람다가 실행 중인 쓰레드는 살아있을 가능성이 있다.

위의 코드에서 a가 있는 쓰레드가 사라져도 람다식의 쓰레드는 살아있을 수 있다는 말이다. 그럼 람다 쓰레드에서 참조하고 있는 a를 참조할 수 없어야 하지않은가? 하지만 오류는 나지 않는다. 그 이유가 Variable Capture 때문이다. 람다에서는 지역변수에 직접 접근하는 것이 아닌 해당 변수를 자신의 스택에 복사해 사용하기 때문이다.

하지만 이러한 경우에 복사된 지역변수가 변경된다면 어떻게 되겠는가? 복사된 변수를 믿고 사용할 수 없게 된다. 따라서 지역 변수에는 final이어야 하거나 final같이 동작해야 한다는 제약조건이 생긴 것이다.



4. 메소드, 생성자 레퍼런스


람다식에서 메소드 참조란 메소드를 참조해 파라미터의 정보나 리턴 타입을 알아내 람다식에서 불필요한 파라미터를 제거하는 것을 말한다.

@FunctionalInterface
public interface MyInterface {
    int sum (int x, int y);
}

public class Main{
	public static void main(String[] args){
		MyInterface myInterface = (x,y) -> x+y;
    }
}

이렇게 람다식을 표현할 수 있을 뿐 아니라 파라미터까지 제거가 가능하다.

public class Main{
	public static void main(String[] args){
    	MyInterface myInterface = Integer::sum;
    }
}

다음과 같이 함수형 인터페이스에서 선언한 추상 메소드의 타입::메소드명으로 작성하는 것이 람다식 메소드참조이다.


람다식 생성자 참조도 비슷하다.

public class Member{
	private String name;
    private int age;
    
    public Member(){
    }
    
    public Member(String name, int age){
		this.name = name;
        this.age = age;
    }
}

@FunctionalInterface
public interface MyInterface {
    Member example(String name, int age);
}

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterface() {
            @Override
            public Member example(String name, int age) {
                return new Member(name, age);
            }
        };
    }
}

이러한 코드의 Main을 람다식으로 바꿔보자.

public class Main{
	public static void main(String[] args){
    	MyInterface myInterface = (name,age) -> new Member(name,age);
    }
}

이렇게 람다식으로 간단해진 코드가 생성자 참조를 하게 되면 다음과 같아 진다.

public class Main{
	public static void main(String[] args){
    	MyInterface myInterface = Member::new;
    }
}


Reference


본 스터디는 2020 백기선님의 자바스터디의 커리큘럼을 참고하여 진행하고 있습니다.

0개의 댓글