본문 바로가기
JAVA

람다식

by 융디's 2024. 5. 3.
728x90
람다식

람다식

@2024.04.24

람다식(Lambda Expression)

💡
자바 8부터 도입되었으며, 함수적 인터페이스의 인스턴스를 생성하는 표현식
주로 익명 함수의 형태로 사용된다.


@기본 구조

(매개변수 리스트) -> {람다 몸체}
  • 함수형 프로그래밍의 개념이 객체 지향 프로그래밍 언어에 통합된 것
  • 람다식 구성 요소
    • 매개 변수 리스트
      • 함수가 받아들이는 매개 변수들의 목록
    • 화살표 ->
      • 매개 변수와 람다 몸체를 구분하는 기호
    • 람다 몸체
      • 실제 람다식의 실행 부분
      • 매개 변수를 사용하여 특정 작업을 수행하거나 그 결과를 반환 가능
  • 특징
    • 익명성
      • 이름이 없기 때문에 익명으로 사용
    • 함수형 인터페이스와 연결
      • 람다 함수는 함수형 인터페이스의 구현체로 사용
      • 함수형 인터페이스란 오직 하나의 추상 메서드를 가진 인터페이스
    • 간결성과 표현력
      • 코드를 간결하게 만들고, 명확한 표현을 가능하게 한다.
    • 지연 실행
      • 람다식은 실행될 필요가 있을 때만 코드가 실행되므로, 성능 향상에 도움
  • 주의 사항
    • 모든 상황에서 익명 클래스를 완전히 대체하지 않는다!
      • 함수적 인터페이스에 대해서만 사용 가능
        • 추상 메서드가 한 개만 있기에 컴파일러가 타입 추론이 가능
      • 인터페이스에 여러 개의 추상 메서드가 있을 경우, 람다식으로 표현 불가능
    • 타입 추론
      • 자바 컴파일러는 대부분의 경우, 매개변수 타입을 추론 가능
      • 따라서 타입을 명시적으로 선언하지 않아도 된다.
    • 매개변수 괄호
      • 매개변수가 하나의 표현식일 경우 괄호의 필요가 없다.
      • 매개변수가 여러 개일 경우에는 괄호가 필요하다.
    • 블록 사용
      • 람다식 본문이 하나의 표현 식인 경우 중괄호가 필요 없다.
      • 여러 줄의 코드를 포함하는 경우에는 중괄호를 사용해야 한다.

람다식 구현과 활용

  • 매개 변수가 없을 경우
    • () → { 동작 }
  • 한 개의 매개 변수만 있을 경우 → 괄호 생략 가능!
    • (a) → { 동작 } or a → { 동작 }
  • 두 개 이상의 매개변수가 있을 경우
    • (a, b) → { 동작 }
  • 단일 표현식 경우 → 중괄호 생략 가능!
    • (a, b) → a + b
  • 복잡한 동작 또는 여러 줄의 코드
    • (a, b) → { 동작 }
  • 반환 값없음
    • () → System.out.println(”안녕하세요”);
  • 반환 값있음
    • () → Math.random();
  • 람다식은 주로 컬렉션의 스트림 API, 스레드 생성, 이벤트 리스너 등 다양한 곳에서 사용
  • 예시 1
    • 리스트의 각 요소 출력
    import java.util.Arrays;
    import java.util.List;
    
    List<String> items = Arrays.asList("Apple", "Banana", "Cherry");
    
    // 람다식을 이용하여 리스트의 각 요소 출력
    items.forEach(item -> System.out.println(item));
  • 예시 2
    • 스레드 생성
    // 익명 클래스를 이용하여 스레드 생성 및 실행
    new Thread(new Runable(){
    	@Override
    	public void run(){
    		for (int i =0; i < 5; i++){
    			System.out.println(i);
    		}
    	}
    }
    ).start();
    
    // 람다식을 이용하여 스레드 생성 및 실행
    new Thread(() -> {
    	for (int i =0; i < 5; i++){
    			System.out.println(i);
    	}
    }).start()

람다식과 인터페이스

함수적 인터페이스(Functional Interface)

💡
단 하나의 추상 메서드를 가진 인터페이스로, 자바 8부터 @FunctionalInterface 애너테이션을 사용하여 함수적 인터페이스임을 명시적으로 표현 가능하다.
  • 람다식을 통해 인터페이스의 추상 메서드 구현을 제공
  • 예시
    • Comparator 인터페이스를 활용하여 문자열 리스트 정렬
    List<String> names = Arrays.asList("Alic", "Bob", "Charlie");
    
    // 방법 1 : 익명 클래스 활용하여 Comparator 구현
    names.sort(new Comparator<String>() {
    		@Override
    		public int compare(String s1, String s2){
    				return s1.compareTo(s2);
    		}
    	});
    System.out.println(names); // [Alice, Bob, Charlie]
    
    // 방법 2 : 람다식을 사용하여 Comparator 구현
    names.sort((String s1, String s2) ->  s1.compareTo(s2));
    System.out.println(names);  // [Alice, Bob, Charlie]

사용자 정의 함수적 인터페이스

💡
자바 표준 API에 포함되지 않으며, 사용자가 필요에 따라 만드는 함수적 인터페이스
  • 특징
    • 단일 추상 메서드
      • 함수적 인터페이스는 단 하나의 추상 메서드를 가져야 한다.
    • @FunctionalInterface 애노테이션
      • 인터페이스가 함수적 인터페이스임을 명시적으로 표시
      • 컴파일러는 애노테이션이 있는 인터페이스에 두 개 이상의 추상 메서드가 있는지 검사
    • 기본 메서드와 정적 메서드 지원
      • 자바 8부터는 인터페이스 내에 기본 메서드와 정적 메서드를 정의 가능
    • 재사용과 유지 보수 용이
      • 일반적인 작업을 위한 인터페이스를 정의
      • 재사용이 가능함으로써 코드의 중복을 줄이고 유지 보수를 용이

  • 함수형 인터페이스를 구현할 때, 람다 표현식을 사용하여 간결하게 표현 가능
    ⇒ 별도의 클래스를 만들지 않고도 인터페이스의 구현체를 직접 정의하고 사용 가능

    Implement 키워드를 사용하지 않고 람다 표현식을 통해 함수형 인터페이스의 구현체를
    정의하고 사용 가능
    // 사용자 정의 함수적 인터페이스 생성
    @FunctionalInterface
    public interface MyInterface {
    		int method(int x);
    }
    
    public class MyFunctionalTest{
    		public static void main(String[] args){
    			MyInterface f;
    			
    			// 익명 함수로 추상 메서드 구현 및 구현 객체 생성
    			f = new MyFunctionalTest() {
    					@Override 
    					public void method(int x){
    						int result = x * 5;
    						System.out.println(result);
    				}
    			};
    			f.method(10);
    			 
    			// 람다 함수로 추상 메서드 구현 및 구현 객체 생성
    			f = x -> {
    					int result = x*5;
    					System.out.println(result);
    			}
    	} 
  • 예시
    • 두 정수의 합, 차, 곱, 나눗셈을 각각 계산하는 람다식
    • add, subtract, multiply, divides ⇒ 인터페이스의 구현체
    // 사용자 정의 함수적 인터페이스 생성
    @FunctionalInterface
    public interface IntBinaryOperation {
    		int apply(int a, int b);
    }
    
    // IntBinaryOperation 인터페이스의 구현체를 가리키도록 람다 표현식으로 초기화 
    // 람다식은 apply() 추상 메서드를 메서드 오버라이딩 한 것
    IntBinaryOperation add = (a, b) -> a + b;
    IntBinaryOperation subtract = (a, b) -> a -b;
    IntBinaryOperation multiply = (a, b) -> a * b;
    IntBinaryOperation divide = (a, b) -> a / b;
    
    System.out.println("10 + 5 = " + add.apply(10, 5));      // 15
    System.out.println("10 - 5 = " + subtract.apply(10, 5)); // 5
    System.out.println("10 * 5 = " + multiply.apply(10, 5)); // 50
    System.out.println("10 / 5 = " + divide.apply(10, 5));   // 2

변수 범위와 람다식

💡
람다식에서 사용되는 지역 변수는 접근이 가능하다.
단, 이 지역변수는 반드시 final 이거나 사실상 fianl인 변수여야 한다.
  • 사실상 fianl인 변수 : final로 선언되지 않았어도 해당 변수에 재할당이 발생되지 않는 경우
    int x = 10;
    
    System.out.println("람다에서 파이널로 사용.");
    
    Runnable r = () -> {
    	System.out.println("x : " + x);
    };
    
    x = 20; // 람다식에서 사용된 지역변수므로, 해당 변수에 재할당 불가능 
    
    r.run()
  • 람다식이 실행되는 시점에는 람다식 안 메서드가 이미 실행을 마치고, 해당 지역 변수가 소멸
  • 람다식은 복사된 변숫값으로 작동되며, 이는 변수가 변경되지 않는 가정하에 안전하게 작동 가능

람다와 자바 표준 API

💡
주요 함수적 인터페이스에 대해서 알아보자.
  • Consumer<T>
    • 하나의 매개변수를 받아서 아무런 반환 값 없이 처리
    • 주로 입력값을 소비하는 작업을 수행하는 데 사용
    • void accept(T t) 구현
      • 주어진 입력값을 사용하여 어떤 처리하며 결과는 반환하지 않는다.
      • 주로 입력값을 변경하거나 출력하는 작업에 사용
    Consumer<String> printer = s -> System.out.println(s);
    printer.accept("hello~ yoon-D"); // hello~ yoon-D
    
    
    Consumer<String> conA = s -> System.out.println(s + "aaaa");
    Consumer<String> conB = s -> System.out.println(s + "bbbb");
    conA.accept("yoon-D"); // yoon-Daaaa
    conB.accept("yoon-D"); // yoon-Dbbbb
    //Consumer 의 andThen 메소드 활용 ConA가 실행되고, conB가 실행된다.
    Consumer<String> conAB = conA.andThen(conB);
    conAB.accept("hello "); // hello aaaa와 hello aaaabbbb
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    
    Consumer<String> printConsumer = name -> System.out.println(name);
    names.forEach(printConsumer);
  • Supplier<T>
    • 매개변수 없이 특정 타입의 객체를 반환하는 함수
    • 주로 객체를 생성하거나, 계산에 필요한 초깃값을 제공하는 데 사용
    • T get() 구현
      • 매개변수 없이 특정 타입의 객체를 반환
    Supplier<Double> randomSupplier = () -> Math.random();
    System.out.println(randomSupplier.get());
    
    IntSupplier intSupplier = () -> (int) (Math.random() * 6) + 1;
    System.out.println(intSupplier.getAsInt());
  • Function<T, R>
    • 하나의 매개변수를 받아서 다른 타입의 결과를 반환하는 함수
    • 주로 입력 값을 변환하거나 매핑하는데 사용
    • R apply(T t) 구현
      • 주어진 입력 값에 대해 어떤 처리를 하고 그 결과를 반환
    Function<String, Integer> lengthFunc = s -> s.length();
    System.out.println(lengthFunc.apply("hello carami!!"));
  • Preditcate<T>
    • 매개 변수 하나를 받아 주어진 조건에 따라 boolean 값을 반환
    • 주로 컬렉션의 요소를 필터링하거나, 어떤 객체가 특정 조건을 만족하는지 검사하는 데 사용
    • boolean test(T t) 구현
      • 주어진 객체에 대해 조건을 검사하고 그 결과를 boolean 값으로 반환
    Predicate<Integer> isPositive = x -> x > 0;
    
    System.out.println(isPositive.test(10));
    System.out.println(isPositive.test(-10));
    
  • UnaryOperatar<T>
    • 입력과 출력이 동일한 타입을 가지는 연산을 수행
    • apply() 구현
    • 주로 자료 구조나 컬렉션의 요소를 변환 시키는데 사용
    UnaryOperator<Integer> square = x -> x * x;
    System.out.println(square.apply(5));
  • BiFunction<T, U, R>
    • 두 개의 입력 매개변수를 받고 결과를 반환하는 함수를 표현
    • T와 U는 매개 변수의 타입을 나타내고, R은 결과의 타입을 나타낸다.
    • R apply(T t, U u) 메서드 구현
    • 주로 두 개의 입력 값을 받아 어떤 처리를 하고 그 결과를 반환하는 데 사용
     //BiFunction
            //두 개의 입력을 받고 하나의 결과를 반환
            BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
    
            System.out.println(add.apply(5, 6));

람다식 확장

메소드 참조

💡
람다식의 확장으로, 특정 메서드의 직접 참조를 통해 코드의 간결성을 더욱 향상시킨다.
  • 람다식이 한 메서드만을 호출하는 경우, 람다식 대신 메서드 참조를 사용
  • 메소드 참조의 형태
    • 정적 메서드 참조
      • 클래스::정적 메서드
      • Math::max → Math 클래스의 정적 메서드 max를 참조
      //BiFunction<Integer, Integer, Integer> maxFunc =(a,b)->Math.max(a,b);
      BiFunction<Integer, Integer, Integer> maxFunc = Math::max;
      System.out.println(maxFunc.apply(3, 5));
    • 인스턴스 메서드 참조
      • 객체 참조 변수::인스턴스 메서드
      • String 객체 str에 대해 str::length legth() 참조
      //2. 인스턴스 메서드 참조
      String str = "Hello world!";
      Supplier<Integer> lengthFunc = str::length;
      System.out.println(lengthFunc.get());
    • 임의 객체의 인스턴스 메서드 참조
      • 클래스::인스턴스 메서드
      • String::length 모든 String 객체의 length() 참
      //3. 임의 객체의 인스턴 메서드 참조
      List<String> words = Arrays.asList("Hello", "world", "java", "carami");
      List<Integer> lengths = new ArrayList<>();
      
      Function<String, Integer> lengthFunc2 = String::length;
      
      for (String word : words) {
          lengths.add(lengthFunc2.apply(word));
      }
      System.out.println(lengths);
    • 생성자 참조
      • 클래스::new
        • ArrayList::new ArrayList 생성자 참조
      //4. 생성자 참조
      Supplier<List<String>> listSupplier = ArrayList::new;
      
      List<String> list = listSupplier.get();
      list.add("hello");
      list.add("hahaha");
      list.add("carami");
      
      System.out.println(list);

728x90

'JAVA' 카테고리의 다른 글

Java Optional  (0) 2024.05.03
Stream  (0) 2024.05.03
익명 객체  (0) 2024.04.27
UDP 프로그래밍  (0) 2024.04.27
TCP 프로그래밍  (1) 2024.04.27