Programming/Java

[Java 자바] 14. 람다식

erinh 2022. 9. 14. 00:26
반응형

14-1. 람다식이란?

- 익명 함수를 생성하기 위한 식으로 객체 지향 언어보다는 함수 지향 언어에 가까움

- "(매개변수) -> {실행코드} " 형태로 작성

// Runnable 인터페이스의 익명 구현 객체를 생성하는 코드
Runnable runnable = new Runnable() {
  public void run() {...}
}

// 람다식을 이용해 표현할 경우
Runnable runnable = () -> {...};

14-2. 람다식 기본 문법

// 기본 형태
(타입 매개변수, ...)  -> { 실행문; ... }

// int 매개 변수 a 값을 콘솔에 출력할 때
(int a) -> {System.out.println(a);}

// 매개 변수 타입은 런타임 시 대입값에 따라 자동으로 인식될 수 있어,
// 람다식에서는 일반적으로 매개변수의 타입을 언급하지 않음
(a) -> {System.out.println(a);}

// 하나의 매개변수만 있다면 괄호() 생략 가능
// 하나의 실행문만 있다면 중괄호{} 생략 가능
a -> System.out.println(a);

// 매개변수가 없을 경우 빈 괄호 사용
() -> {실행문, ...}

// 중괄호를 실행하고 결과값을 리턴해야 할 경우
(x, y) -> { return x + y; }

// 중괄호에 return문만 있을 경우, return과 중괄호 생략 가능
(x, y) -> x + y

14-3. 타겟 타입과 함수적 인터페이스

- 람다식은 단순히 메소드를 선언하는 것이 아닌, 메소드를 가지고 있는 객체를 생성

- 람다식은 인터페이스 변수에 대입되어, 인터페이스의 익명 구현 객체를 생성함

- 람다식의 타겟 타입: 람다식이 대입될 인터페이스

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

14-3-1. 함수적 인터페이스 (@FunctionalInterface)

- 람다식은 하나의 메소드를 정의하기 때문에 두 개 이상의 추상 메소드가 선언된 인터페이스는 람다식을 이용할 수 없음
=> 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타겟 타입이 될 수 있으며, 이러한 인터페이스를 함수적 인터페이스라 함

- @FunctionalInterface 어노테이션을 통해 두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹해줌

@FunctionalInterface
public interface MyFunctionalInterface {
  public void method();
  public void otherMethod();	// 컴파일 오류
}

14-3-2. 매개 변수와 리턴값이 없는 람다식

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

// MyFunctionalInterfaceTest.java
public class MyFunctionalInterfaceTest {
  public static void main(String[] args) {
    MyFunctionalInterface fi;
    
    // method()의 매개변수가 없으므로 람다식의 매개변수도 없음
    fi = () -> {
      String str = "method call1";
      System.out.println(str);
    };
    fi.method();
    
    fi = () -> { System.out.println("method call2"); };
    fi.method();
    
    fi = () -> System.out.println("method call3");
    fi.method();
  }
}

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

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

// MyFunctionalInterfaceTest.java
public class MyFunctionalInterfaceTest {
  public static void main(String[] args) {
    MyFunctionalInterface fi;
    
    fi = (x) -> {
      int result = x * 5;
      System.out.println(result);
    };
    fi.method(2);
    
    fi = (x) -> {System.out.println(x*5);};
    fi.method(2);
    
    fi = x -> System.out.println(x*5);
    fi.method(2);
  }
}

14-3-4. 매개 변수가 있고 리턴값이 있는 람다식

// MyFunctionalInterface.java
@FunctionalInterface
public interface MyFunctionalInterface {
  public int method(int x, int y);
}

// MyFunctionalInterfaceTest.java
public class MyFunctionalInterfaceTest {
  public static void main(String[] args) {
    MyFunctionalInterface fi;
    
    fi = (x, y) -> {
      int result = x + y;
      return result;
    };
    System.out.println(fi.method(2, 5));
    
    fi = (x, y) -> { return x + y; };
    System.out.println(fi.method(2, 5));
    
    fi = (x, y) -> x + y;
    System.out.println(fi.method(2, 5));
    
    fi = (x, y) -> sum(x, y);
    System.out.println(fi.method(2, 5));
  }
  
  public static int sum (int x, int y) {
    return (x + y);
  }
}

14-4. 클래스 멤버와 로컬 변수 사용

14-4-1. 클래스 멤버 사용

- 람다식 실행 블록에서는 클래스의 멤버인 필드와 메소드를 제약 사항 없이 사용 가능
- 단, this 키워드를 사용할 경우 익명 객체의 참조가 아닌 람다식을 실행한 객체의 참조이므로 주의

// MyFunctionalInterface.java
public interface MyFunctionalInterface {
  public void method();
}

// UsingThis.java
public class UsingThis {
  public int outterField = 10;
  
  class Inner {
    int innerField = 20;
    
    void method() {
      // 람다식
      MyFunctionalInterface fi = () -> {
        System.out.println("outterField: " + outterField);
        // 바깥 객체의 참조를 얻기 위해서는 클래스명.this 사용
        System.out.println("outterField: " + UsingThis.this.outterField + "\n");
        
        System.out.println("innerField: " + innerField);
        System.out.println("innerField: " + this.innerField + "\n");  
      };
      fi.method();
    }
  }
}

// UsingThisTest.java
public class UsingThisTest {
  public static void main(String[] args) {
    UsingThis usingThis = new UsingThis();
    UsingThis.Inner inner = usingThis.new Inner();
    inner.method();
  }
}

14-4-2. 로컬 변수 사용

- 람다식은 메소드 내부에서 주로 작성되기에 로컬 익명 구현 객체를 생성시킨다고 할 수 있음
- 메소드의 매개변수 또는 로컬 변수를 사용하려면 이 두 변수는 final 특성을 가져야 함

// MyFunctionalInterface.java
public interface MyFunctionalInterFace {
  public void method();
}

// UsingLocalVariable.java
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();
  }
}

//UsingLocalVariableTest.java
public class UsingLocalVariableTest {
  public static void main(String[] args) {
    UsingLocalVariable ulv = new UsingLocalVariable();
    ulv.method(20);
  }
}

14-5. 표준 API의 함수적 인터페이스

- 자바 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스는 모두 람다식을 이용해 익명 객체 구현 가능 

14-5-1. Consumer 함수적 인터페이스

- 리턴값이 없는 accept() 메소드를 가지고 있음
- accept() 메소드는 매개값을 받아 소비하는 역할만 함 (사용만 할 뿐 리턴값이 없다)

- 매개 변수와 타입에 따라 여러 Consumer 인터페이스가 존재함

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

public class ConsumerExample {
  public static void main(String[] args) {
    Consumer<String> consumer = t -> System.out.println(t + "8");
    consumer.accept("Java");
  }
}

14-5-2. Supplier 함수적 인터페이스

- 매개 변수가 없고 리턴값이 있는 getXXX() 메소드를 가지고 있음
- 메소드는 실행 후 호출한 곳으로 데이터를 리턴(공급)하는 역할을 함
- get() 메소드가 매개값을 가지지 않으므로 람다식도 ()를 사용

인터페이스명 추상 메소드 설명
Supplier<T> T get() T 객체를 리턴
BooleanSupplier boolean getAsBoolean() boolean 값을 리턴
DoubleSupplier double getAsDouble() doulbe 값을 리턴
IntSupplier int getAsInt() int 값을 리턴
LongSupplier long getAsLong() long 값을 리턴
// SupplierTest.java

public class SupplierTest {
  public static void main(String[] args) {
    IntSupplier intSupplier = () -> {
      int num = (int) (Math.random() * 6) + 1;
      return num;
    };
    
    int num = intSupplier.getAsInt();
    System.out.println("눈의 수: " + num);
  }
}

14-5-3. Function 함수적 인터페이스

- 매개값과 리턴값이 있는 applyXXX() 메소드를 가지고 있음
- 매개값을 리턴값으로 매핑(타입 변환) 하는 역할을 함

// Student.java
public class Student {
  private String name;
  private int englishScore;
  private int mathScore;
  
  public Student(String name, int englishScore, int mathScore) {
    this.name = name;
    this.englishScore = englishScore;
    this.mathScore = mathScore;
  }
  
  public String getName() {return name;}
  public int getEnglishScore() {return englishScore;}
  public int getMathScore() {return mathScore;}
}

// FunctionTest.java

public class FunctionTest {
  private static List<Student> list = Arrays.asList(
    new Student("a", 90, 96),
    new Student("b", 95, 93)
  );
  
  public static void printString(Function<Student, String> function) {
    for(Student student: list {
      System.out.print(function.apply(student) + " ");
    }
    System.out.println();
  }
  
  public static void printInt (ToIntFunction<Student> function) {
    for(Student student : list {
      System.out.print(function.applyAsInt(student) + " ");
    }
    System.out.println();
  }
  
  public static void main(String[] args) {
    System.out.println("[학생 이름]");
    printString(t -> t.getName());
    
    System.out.println("[영어 점수]");
    printInt(t -> t.getEnglishScore());
    
    System.out.println("[수학 점수]");
    printInt(t -> t.getMathScore());
  }
}

14-5-4. Operator 함수적 인터페이스

- Function과 동일하게 매개 변수와 리턴값이 있는 applyXXX() 메소드를 가지고 있음

- 하지만 매개값을 이용해 연산을 수행한 후 동일한 타입으로 리턴값을 제공한다는 점에서 차이

// OperatorTest.java

public class OperatorTest {
  private static int[] scores = {92, 95, 87};
  
  public static int maxOrMin (IntBinaryOperator operator) {
    int result = scores[0];
    for(int score = scores) {
      result = operator.applyAsInt(result, score);
    }
    return result;
  }
  
  public static void main(String[] args) {
    int max = maxOrMin(
      (a, b) -> {
        if (a >= b) return a;
        else return b;
      }
    );
    
    int min = maxOrMin(
      (a, b) -> {
        if (a <= b) return a;
        else return b;
      }
    );
    System.out.println("최소값 :" + min);
  }
}
반응형