[이것이자바다] chapter 14. 람다식

chapter 14

람다식

람다식이란?

Runnable runnable = () -> {...}; // 람다식
  • 위 코드는 Runnable 변수에 대입되므로 람다식은 Runnable의 익명 구현 객체를 생성하게 된다.

=>람다식은 함수적 프로그래밍 언어로 “(매개변수)->{실행코드}” 형태로 작성된다. 병렬 처리와 이벤트 지향 프로그래밍에 적합한 언어로 자바에서는 한때는 주목받지 못하다가 요즘들어 다시 각광받고 있다. 각광받는 이유는 람다식을 사용하면 자바 코드가 간결해지고, 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있기 때문이다.

타겟 타입과 함수적 인터페이스

=> 람다식의 형태가 매개변수가 있는 블록이기 때문에 마치 자바에서 직접 메소드를 선언한 것처럼 보이지만 자바는 메소드를 단독으로 선언 할 수 없기 때문에람다식은 단순히 메소드를 선언하는 것이 아니라 이 메소드를 가지고 있는 객체를 생성해낸다.(명심!)

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

함수적 인터페이스(@FunctionalInterface)

=> 모든 인터페이스는 람다식의 타켓 타입으로 사용할 수 없다. 람다식이 하나의 메소드를 정의하기 때문에 두 개 이상의 추상메소드가 선언된 인터페이스는 람다식을 이용해서 구현 객체를 생성할 수 없다. 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타겟 타입이 될 수 있는데 , 이러한 인터페이스를 함수적 인터페이스(functional interface)라고 한다.

리턴값과 매개변수가 없는 람다식
``java public interface MyFunctionalInterface { void method(); // 추상 메소드 public static void main(String[] args) {

	//매개변수와 리턴 값이 없는 메소드
	MyFunctionalInterface fi = ()->{
		System.out.println("hello");
	};
	fi.method();
} } ```

매개변수가 있는 람다식

public interface ParameterInterface {

	//매개변수가 있는 추상 메소드
	void method(int x);

	public static void main(String[] args) {
		//매개변수가 하나인 경우에는 괄호를 생략할 수 있다.
		ParameterInterface fi = x-> System.out.println("hello "+x);

		fi.method(5);
	}
}

리턴값이 있는 람다식

public interface ReturnInterface {
	//리턴 값이 있는 추상 메소드
	int method();


	public static void main(String[] args) {
		//리턴값이 있는 람다식
		ReturnInterface fi = ()->{
			return 5;
		};

		int x = fi.method();
		System.out.println("hello "+ x);
	}

}

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


public interface ParameterReturnInterface {
	//리턴값과 매개변수가 둘다 있는 추상 메소드
	int add(int x, int y);

	public static void main(String[] args) {
		// 리턴값과 매개변수가 있는 람다식
		ParameterReturnInterface fi = (x, y) ->{
			return x+y;
		};

		int result = fi.add(3,5);

		System.out.println("3 과 5를 더하면 " + result+ "입니다.");
	}

}

클래스 멤버와 로컬 변수 사용

클래스의 멤버 사용

=> 람다식 실행 블록에는 클래스의 멤버인 필드와 메소드를 제약 없이 사용할 수 있는데, this의 사용에는 주의를 기울이자. 일반적으로 익명 객체 내부에서 this는 익명 객체의 참조이지만, 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조이다.

public class UsingThis{
	public int outterfield = 10;

	class Inner{
	  int innerField = 20;

	  void method(){
      //Ramda 
      MyFunctionInterface fi = () -> {
       
        /* 람다식에서의 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조이다. 즉 Inner 클래스 인스턴스의 참조이다 .*/ 

        //바깥 객체의 참조를 얻기 위해서는 클래스명 this를 사용
        System.out.println("outterField: " + UsingThis.this.outterField + "\n");
        
        
        //람다식 내부에서 this는 Inner 객체를 참조
        System.out.println("innnerField: " + this.innerField + "\n");

			};

		}
	
	}

}

로컬 변수 사용

public class UsingLocalVariable {
	void method(int arg){ //arg는 매개변수이기 때문에 final 한 특성을 가짐.
    int localVar = 40; // localVar은 로컬 변수이기 때문에 final한 특성을 가짐

 
    //arg = 5;(x) : arg는 람다식에서 사용하므로 매개변수로서 final한 특성을 가져야 하므로 값이 변경될 수 없다.
    //localVar = 20;(x) : localVar 은 람다식에서 사용하므로 로컬 변수로서 final한 특성을 가져야 하기 때문에 값이 변경 될 수 없다.

    MyFunctionalInterface fi = () ->{
      //로컬 변수 읽기
      System.out.println("arg: " + arg);
      System.out.println("localVar: " + localVar);
		};
    fi.method();
	}

표준 API의 함수적 인터페이스

public class RunnableExample {

  public static void main(String args[]){
    
    // 이 람다식은 Runnable 인터페이스의 run() 메소드를 나타낸다.
    // 즉, 스레드가 실행하는 코드이다.
    Runnable runnable = () ->{
      for(int i=0; i<10; i++){
         System.out.println(i);

			}

		};

    Thread thread = new Thread(runnable);
    thread.start();


  }
}

* **Thread 생성자를 호출할  다음과 같이 람다식을 매개값으로 대입해도 된다.(대입하자)**

```java
Thread thread = new Thread(()->{
  for(int i=0; i<10; i++){
    System.out.println(i);
	}

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

Consumer 함수적 인터페이스

=> Consumer 함수적 인터페이스의 특징은 리턴값이 없는 accept() 메소드를 가지고 있다. accept() 메소드는 단지 매개값을 소비하는 역할만 한다. 여기서 소비한다는 말은 사용만 할 뿐 리턴값이 없다는 뜻이다.

인터페이스명 추상 메소드 설명
Consumer<T> void accept(T t) 객체 T를 받아 소비
BiConsumer<T,U> 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 값을 받아 소비

예제

import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.DoubleConsumer;
import java.util.function.ObjIntConsumer;

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

        BiConsumer<String, String> biConsumer = (t,u) -> System.out.println(t+u);
        biConsumer.accept("java","8");

        DoubleConsumer doubleConsumer = (t) -> System.out.println("java"+t);
        doubleConsumer.accept(8.0);

        ObjIntConsumer<String> objIntConsumer = (t,i) -> System.out.println(t+i);
        objIntConsumer.accept("java",8);
    }
}

Supplier 함수적 인터페이스

=> Supplier 함수적 인터페이스의 특징은 매개 변수가 없고 리턴값이 있는 getXXX() 메소드를 가지고 있다. 이 메소드들은 실행 후 호출한 곳으로 데이터를 리턴(공급)하는 역할을 한다.

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

import java.util.function.IntSupplier;

public class SupplierExample {

    public static void main(String[] args) {
        IntSupplier intSupplier = ()->{
            //1~6에서 random한 숫자가 나올 것.
            return (int)(Math.random()*6) + 1;
        };

        int num = intSupplier.getAsInt();
        System.out.println(num);
    }

Functuon 함수적 인터페이스

=> Function 함수적 인터페이스의 특징은 매개값과 리턴값이 있는 applyXXX() 메소드를 가지고 있다. 이 메소드들은 매개값을 리턴값으로 매핑(타입 변환)하는 역할을 한다.

인터페이스명 추상 메소드 설명
Function<T,R> R apply(T t) 객체 T를 객체 R로 매핑
BiFunction<T,U,R> R apply(T t,U u) 객체 T와 U를 객체 R로 매핑
DoubleFunction<R> R apply(double value ) double을 객체 R 로 매핑
IntFunction<R> R apply(int value) int를 객체 R로 매핑
IntToDoubleFunction double applyAsDouble(int value) int를 double 로 매핑
IntToLongFunction long applyAsLong(int value) int를 long으로 매핑
LongToDoubleFunction double applyAsDouble(long value) long을 double로 매핑
LongToIntFunction double applyAsDouble(long value) long을 double로 매핑
ToDoubleBiFunction<T,U> double applyAsDouble(T t, U t) 객체 T와 U를 double로 매핑
ToDoubleFunction<T> double applyAsDouble(T t) 객체 T를 double로 매핑
ToIntBiFunction<T,U> int applyAsInt(T t, U t) 객체 T와 U를 int로 매핑
ToIntFunction<T> int applyAsInt(T t, U t) 객체 T를 int로 매핑
ToLongBiFunction<T,U> long applyAsLong(T t, U t) 객체 T와 U를 long으로 매핑
ToLongFunction<T,U> long applyAsLong(T t) 객체 T를 long으로 매핑
Function<Student, String > function = t -> {return t.getName();}
또는 
Function<Student, String > function = t -> t.getName();

=> 리턴문만 있을 경우 중괄호 {} 와 return문은 생략할 수 있다.

ToIntFunction<Student> function = t-> { return t.getScore();}
또는 
ToIntFunction<Student> functoin = t -> t.getScore();

=> 이 경우도 역시 리턴문만 있을 경우 중괄호 {}와 return문은 생략할 수 있다.



import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.ToIntFunction;

public class FunctionExample1 {

    private static List<Student> list = Arrays.asList(
            new Student("홍길동", 90, 96),
            new Student("김도형", 100, 100)
    );

    //function에 인자가 t-> t.getName();으로 전달되므로 Student의 getName()이 리턴된다.
    private static  void printString(Function<Student, String> function) {
        for (Student student : list) {
            System.out.println(function.apply(student) + " ");
        }
        System.out.println();
    }

    //function에 인자가 t-> t.getEnglishScore();으로 전달되므로 Student의 getEnglishScore()이 리턴된다.
    private static  void printInt(ToIntFunction<Student> function) {
       for(Student student: list) {
           System.out.println(function.applyAsInt(student));
       }
        System.out.println();
    }

    //function에 인자가 t-> t.getMathScore();으로 전달되므로 Student의 getMathScore()이 리턴된다.

    public static void main(String[] args) {
        System.out.println("[학생 이름]");
        FunctionExample1.printString(t->t.getName());

        System.out.println("[영어 성적]");
        FunctionExample1.printInt(t->t.getEnglishScore());

        System.out.println("[수학 성적]");
        FunctionExample1.printInt(t->t.getMathScore());
    }
}
import java.util.Arrays;
import java.util.List;
import java.util.function.ToIntFunction;

public class FunctionExample2 {

    private static List<Student> list = Arrays.asList(
            new Student("홍길동", 90,90),
            new Student("김도형", 100, 100)
    );

    private static double avg(ToIntFunction<Student> function){
        int sum =0;
        for(Student student: list){
            sum+= function.applyAsInt(student);
        }
        return (double) sum/ list.size();
    }

    public static void main(String[] args){
        System.out.println("[영어 평균]");
        System.out.println(avg(t->t.getEnglishScore()));

        System.out.println("[수학 평균]");
        System.out.println(avg(t->t.getMathScore()));
    }
}

Operator 함수적 인터페이스

=> Operator 함수적 인터페이스는 Function 와 동일하게 매개 변수와 리턴 값이 있는 applyXXX() 메소드를 가지고 있다. 하지만 이 메소드들은 매개값을 리턴값으로 매핑(타입 변환)하는 것보다 매개값을 이용해서 연산을 수행한 후 동일한 타입으로 리턴값을 제공하는 역할을 한다.

인터페이스명 추상 메소드 설명
BinaryOperator<T> T apply(T t, T t) T와 T를 연산한 후 T 리턴
UnaryOperator<T> T apply(T t) T를 연산한 후 T 리턴
DoubleBinaryOperator double applyAsDouble(double ,double) 두 개의 double 연산
DoubleUnaryOperator double applyAsDouble(double) 한 개의 double 연산
IntBinaryOperator int applyAsInt(int , int) 두 개의 int 연산
IntUnaryOperator int applyAsInt(int) 한 개의 int 연산
LongBinaryOperator long applyAsLong(long, long) 두 개의 long 연산
LongUnaryOperator long applyAsLong(long) 한 개의 long 연산
import java.util.function.IntBinaryOperator;

public class OperatorExample {

    private static int[] scores = {92,95,87};

    //IntBinaryOperator :  두 개의 int 연산.
    private 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 b;
            else return a;
        });

        System.out.println("최댓값: " + Max);
        System.out.println("최솟값: " + Min);
    }
}

Predicate 함수적 인터페이스 => Predicate 함수적 인터페이스는 매개 변수와 boolean 리턴값이 있는 testXXX() 메소드를 가지고 있다. 이 메소드들은 매개값을 조사해서 true 또는 false를 리턴하는 역할을 한다.

인터페이스명 추상 메소드 설명
Predicate<T> boolean test(T t) 객체 T를 조사
BiPredicate<T,U> boolean test(T t, U u) 객체 T 와 U를 조사
DoublePredicate boolean test(double value) double 값을 조사
IntPredicate boolean test(int value) int 값을 조사
LongPredicate boolean test(long value) long 값을 조사
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class PredicateExample {

   private static List<Student2> list = Arrays.asList(
     new Student2("Kim", "man", 90),
     new Student2("Jang", "woman", 95),
     new Student2("Ham", "man",84),
     new Student2("Park", "woman", 88)
   );


   private static double arg(Predicate<Student2> predicate){
       int count =0, sum =0;

       for(Student2 student: list) {
           if (predicate.test(student)) {
              count++;
              sum += student.getScore();
           }
       }
       return (double)sum/count;
   }

    public static void main(String[] args) {
       double arg_Man= arg(t->t.getSex().equals("man"));
       double arg_Woman = arg(t-> t.getSex().equals("woman"));

        System.out.println(arg_Man);
        System.out.println(arg_Woman);
    }
}

andThen() 과 compose() 디폴트(default) 메소드

andThen() 코드

인터페이스AB = 인터페이스A.andThen(인터페이스B);
최종결과 = 인터페이스AB.method();

=> 인터페이스AB의 method()를 호출하면 우선 인터페이스A부터 처리하고 결과를 인터페이스 B의 매개값으로 제공한다. 인터페이스 B는 제공받은 매개값을 가지고 처리한 후 최종 결과를 리턴한다.

compose() 코드

인터페이스 AB = 인터페이스A.compose(인터페이스 B);
최종 결과 = 인터페이스AB.method();

=> compose()는 인터페이스 AB의 method()를 호출하면 우선 인터페이스 B부터 처리하고 결과를 인터페이스A의 매개값으로 제공한다. 인터페이스 A는 제공받은 매개값을 가지고 처리한 후 최종 결과를 리턴한다.

  1. Consumer의 순차적 연결 => Consumer 종류의 함수적 인터페이스는 처리 결과를 리턴하지 않기 때문에 andThen() 디폴트 메소드는 함수적 인터페이스의 호출순서만 정한다. 다음 예제는 Consumer<Member> 함수적 인터페이스 두개를 순차적으로 연결해서 실행한다.

  2. Function의 순차적 연결

=> Function과 Operator 종류의 함수적 인터페이스는 먼저 실행한 함수적 프로그래밍의 결과를 다음 함수적 인터페이스의 매개값으로 넘겨주고, 처리 결과를 리턴한다. 예를 들어, Function<Member, Address>와 Function<Address, String> 을 순차적으로 연결해서 Function<Member,String>을 생성할 수 있다.
=> Function<Member,Address> + Function<Address, String> = Function<Member,String>이 되는데 ,즉 Address는 두 함수적 인터페이스 간의 전달 데이터이고, 최종 함수적 인터페이스의 형태는 입력 데이터가 Member, 출력 데이터가 String이 되는 Function<Member,String> 이 된다.


import java.util.function.Function;

public class FunctionAndThenComposeExample {

    public static void main(String[] args) {
        Function<Member,Address> functionA = m ->m.getAddress();
        Function<Address,String> functionB = a -> a.getCity() ;
        String city;


        Function<Member,String> functionAB = functionA.andThen(functionB);

        city=functionAB.apply(new Member("홍길동", "hong", new Address("korea", "seoul")));
        System.out.println(city);


        functionAB = functionB.compose(functionA);
        city = functionAB.apply(new Member("김도형","kimdo", new Address("korea","suwon")));
        System.out.println(city);


    }
}

and(),or(), negate() 디폴트 메소드

  • Predicate 종류의 함수적 인터페이스는 and(), or(), negate() 디폴트 메소드를 가지고 있다. 이 메소드들은 각각 논리 연산자인 **&&,   , !**과 대응된다고 볼 수 있다.
import java.util.function.IntPredicate;

public class PredicateAndOrNagateExample {
    public static void main(String[] args) {
        //2의 배수 검사
        IntPredicate predicateA = a -> a%2 == 0;

        //3의 배수 검사
        IntPredicate predicateB = a -> a%3 == 0;

        IntPredicate predicateAB;
        boolean result;

        predicateAB = predicateA.and(predicateB);
        result = predicateAB.test(9);
        System.out.println("9는 2와 3의 배수입니까? " +result);

        predicateAB = predicateA.or(predicateB);
        result = predicateAB.test(9);
        System.out.println("9는 2나 3의 배수입니까? " +result);

        predicateAB = predicateA.negate();
        result = predicateAB.test(9);
        System.out.println("9는 2의 배수입니까? " +!result);

        predicateAB = predicateB.negate();
        result = predicateAB.test(9);
        System.out.println("9는 3의 배수입니까? "+ !result);
    }
}

isEqual() 정적 메소드

  • Predicate<T> 함수적 인터페이스는 이외에 isEqual() 메소드를 제공한다. isEqual() 메소드는 test() 매개값인 sourceObject 와 isEqual()의 매개값인 targetObject를 java.util.Objects 클래스의 equals()의 매개값으로 제공하고, Objects.equals(sourceObject, targetObject)의 리턴값을 얻어 새로운 Predicate<T>를 생성한다.
Predicate<Object> predicate = Predicate.isEqual(targetObject);
boolean result = predicate.test(sourceObject);
//Objects.equals(sourceObject, targetSource) 로 값 리턴!

import java.util.function.Predicate;

public class PredicateIsEqualExample {
    public static void main(String[] args) {
        Predicate<String> predicate;

        predicate = Predicate.isEqual(null);
        System.out.println("null, null: " + predicate.test(null));

        predicate = Predicate.isEqual(null);
        System.out.println("null, not null: "+ predicate.test("java8"));

        predicate = Predicate.isEqual("java8");
        System.out.println("not null, null: " + predicate.test(null));

        predicate = Predicate.isEqual("java8");
        System.out.println("not null, not null: " + predicate.test("java8"));
    }
}

minBy(), maxBy() 정적 메소드

  • BinaryOperator<T> 함수적 인터페이스는 minBy()와 maxBy() 정적 메소드를 제공한다. 이 두 메소드는 매개값으로 제공되는 Comparator를 이용해서 최대T와 최소T를 얻는 BinaryOperator<T> 를 리턴한다.
리턴 타입 정적 메소드
BinaryOperator<T> minBy(Comparator<? super T> comparator)
BinaryOperator<T> maxBy(Comparator<? super T> comparator)
@FunctionInterface 
public interface Comparator<T>{
  public int compare(T o1, T o2);
}

public class OperatorMinByMaxByExample {

public static void main(String[] args) {
    BinaryOperator<Fruit> binaryOperator;
    Fruit fruit;

    binaryOperator = BinaryOperator.minBy(
            (f1,f2)-> Integer.compare(f1.getPrice(),f2.getPrice())
    );
    // binaryOperator.apply()는 T와 T를 연산한 후 T를 리턴.
    fruit = binaryOperator.apply(new Fruit("딸기",6000),
            new Fruit("수박", 10000));
    System.out.println("더 저렴한 과일: "+ fruit.getName());

    binaryOperator = BinaryOperator.maxBy(
            (f1,f2) -> Integer.compare(f1.getPrice(),f2.getPrice())
    );
    fruit = binaryOperator.apply(new Fruit("딸기",6000),
            new Fruit("수박", 10000));
    System.out.println("더 비싼 과일: " + fruit.getName());
} } ```