공부/Java

[Java] FunctionalInterface와 Lambda Expression

leejinwoo1126 2023. 7. 28. 21:03
반응형

@FunctionalInterface 

-  하나의 추상 메서드(Single Abstract Method) 만을 가지는 인터페이스

- static / default method 선언 가능 (Java 8 부터)

- java.lang.Runnable, java.util.Comparator 등

     
Function<T, R> R apply(T t);  
BiFunction<T, U, R>  R apply(T t, U u);  
Supplier<T> T get();  
Consumer<T> void accept(T t); T타입의 인자를 받아 로직 수행 (리턴 x)
BiConsumer<T, U> void accept(T t, U t); T, U 타입 인자를 받아 로직 수행 (리턴x)
Predicate<T> boolean test(T t);  
Comparator<T> int compare(T o1, T o2); o1 < o2  (음수, 오름차순)
o1 == o2 (동일한 경우)
o1 > o2  (양수, 내림차순)

 

Lambda Expression 

- 함수형 인터페이스를 가장 쉽게 구현할 수 있도록 해주는 방법 

- 파라미터가 함수형 인터페이스의 경우 함수 인자(람다 표현식) 전달 가능

 

 

자바에서 제공해주는 @FunctionalInterface 

interface Function<T, R> 

T : 매개변수 타입, R : 반환 타임

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Function<T, R> { // 제네릭 인터페이스
   R apply(T var1);
 	
   //..
}

 

(1) Function<T, R> 인터페이스 구현 하여 사용하는 경우

public class Adder implements Function<Integer, Integer> {
   @Override
   public Integer apply(Integer t) {
     return 10 + t;  // 단순히 인자값에 10 더함
   }
}
public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> myAdder = new Adder(); // 인스턴스 생성
        System.out.println(myAdder.apply(5)); // 15
    }
}

simple 하게 Function 인터페이스 구현하여 사용한 형태이지만, apply() method 하나만 구현하여 사용하는데다 생성자 선언까지 하는게 번거롭다. 이때 람다 표현식을 사용하게 되면 간소화 시킬 수 있다. ( Java 8 이상 )

 

 

(2) 람다 표현식 사용하는 경우

public static void main(String[] args) {
    Function<Integer, Integer> myAdder = (Integer a) -> {
        return a + 10;
    };

    System.out.println(myAdder.apply(5)); // 15 
}

익명 함수(Annonymous function)을 사용하게 될 경우 구현 클래스를 인스턴스화 하여 사용하지 않아도 간단히 동일한 기능이 동작하가능하다.

 

추가로 조건 하에 아래와 같이 람다 표현식을 간소화 시키는 것도 가능하다.

/*
1. 매개변수 타입 유추 가능할 경우 타입 생략 가능
2. 매개변수가 하나일 경우 괄호 생략 가능   
3. 바로 리턴하는 경우 중괄호, return 생략 가능
*/
Function<Integer, Integer> myAdder = x -> x + 10;

 

람다 표현식 사용시 Java Compiler는 아래의 정보를 통해 target type을 결정하게 된다

  • Variable declarations
  • Assignments
  • Return statements 
  • Array initializers 
  • Method or constructor arguments 
  • Lambda expression bodies 
  • Conditional expressions, ?:
  • Cast expressions

 

interface BiFunction<T, U, R> 

R apply(T var1, U var2) : T, U 타입의 매개변수를 받아 R 타입을 리턴하는 인터페이스

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface BiFunction<T, U, R> {
   R apply(T var1, U var2);
   
   // ..
}

 

마찬가지로 람다 표현식으로 간단하게 덧셈 연산 함수를 만들 수 있다

import java.util.function.BiFunction;

public class Main {
   
   public static void main(String[] args) {
      BiFunction<Integer, Integer, Integer> simpleAdder = (a, b) -> a + b;
      System.out.println(simpleAdder.apply(2, 8)); // 10
   }
   
}

 

참고. 매개변수 타입 3개인 @FunctionalInterface 인터페이스 만들기

(1) 매개변수를 3개 받는 FunctionInterface 선언

@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

 

(2) TriFunction 인터페이스 람다 구현

public class Main {
  public static void main(String[] args) {
    TriFunction<Integer, Integer, Integer, Integer> triAdder = (a, b, c) -> a + b + c;
    System.out.println(triAdder.apply(1,2, 3)); // 6
  }
}

 

Supplier<T>

T get() : T 타입의 리턴값을 반환하는 추상 메서드

package java.util.function;

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

 

예시1

Supplier<String> myStringSupplier = () -> "hell word";
System.out.println(myStringSupplier.get()); // hell word

 

예시2.

public class Ch4 {
    public static void main(String[] args) {
        // 함수가 1등 시민이 되었으므로, 매개변수로 인자로 전달 가능
        Supplier<Double> myRandomSupplier = () -> Math.random();
        printRandomDouble(myRandomSupplier, 5);
    }

    private static void printRandomDouble(Supplier<Double> supplier, int count) {
        for(int i = 1;  i <= count; i++) {
            System.out.println(supplier.get());
        }
    }
}


[출력 결과]
0.1601446203176864
0.6172793918604935
0.8425539834564604
0.26516593030460756
0.8304236464480694

 

Consumer<T>

void accept(T var1) : T 타입의 인자를 받아 내부 로직 처리 수행만 함 (리턴 x)

@FunctionalInterface
public interface Consumer<T> {
    void accept(T var1);
    
    // ..
}

 

예시1.

단순 String 인자 전달 받아 출력

//1. 익명 함수 선언 사용
Consumer<String> myStringConsumer = (String str) -> {
   System.out.println(str);
};

myStringConsumer.accept("hello world!"); // hello world 


//2. Method Reference(함수 참조) 사용
Consumer<String> myStringConsumer = System.out::println;
myStringConsumer.accept("hello world!");

 

예시2.

static method 인자로 여러 Consumer 함수를 전달하여 활용

public class Main {
    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3);
        Consumer<Integer> myIntegerProcess = x -> System.out.printf("Process Integer 1 : %s%n", x);
        process(integers, myIntegerProcess);

        List<Integer> integers2 = Arrays.asList(4, 5, 6);
        Consumer<Integer> myIntegerProcess2 = x -> System.out.printf("Process Integer 2 : %s%n", x);
        process(integers2, myIntegerProcess2);

        List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
        Consumer<Double> myDoubleProcess = x -> System.out.printf("Process Double : %f%n", x);
        process(doubles, myDoubleProcess);
    }
	
    // 제네릭 매개변수 타입 선언
    public static <T> void process(List<T> inputs, Consumer<T> consumer) {
        for(T input : inputs) {
            consumer.accept(input);
        }
    }
}


[출력 결과]
Process Integer 1 : 1
Process Integer 1 : 2
Process Integer 1 : 3
Process Integer 2 : 4
Process Integer 2 : 5
Process Integer 2 : 6
Process Double : 1.100000
Process Double : 2.200000
Process Double : 3.300000

 

참고. Arrays.asList() 와 new ArrayList() 

- Arrays.asList()은 가변 인자로 전달받은 값을 ArrayList (Arrays.class 위치한 static class ArrayList 호출) 로 반환

- 이때 값이 들어가는 E[]이 final이기 때문에 immutable(불변) 

Arryas.class

 반대로 new ArrayList() 의 경우 mutable 하다

ArrayList.class

BiConsumer<T, U>

Consumer에서 매개변수가 2개로 늘어난 형태

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T var1, U var2);
    
    // ..
}

 

예시

public class Main {
    public static void main(String[] args) {
        // Integer와 Double 타입 인자 받아 처리
        BiConsumer<Integer, Double> myBiConsumer =
                (index, value) -> System.out.printf("Processing index %s, value %f%n", index, value);

        List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3, 4.4);
        process(doubles, myBiConsumer);
    }

    public static <T> void process(List<T> input, BiConsumer<Integer, T> biConsumer) {
        for(int i = 0; i < input.size(); i++) {
            biConsumer.accept(i, input.get(i));
        }
    }
}

 

Predicate<T>

- boolean test(T t) : T 타입 인자를 받아 조건에 따라 true/false(boolean) 반환 

- default, static method 의 경우에도 논리 연산 형태를 보임

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Predicate<T> {
   boolean test(T var1);

   default Predicate<T> and(Predicate<? super T> other) {
      Objects.requireNonNull(other);
      return (t) -> {
         return this.test(t) && other.test(t);
      };
   }

   default Predicate<T> negate() {
      return (t) -> {
         return !this.test(t);
      };
   }

   default Predicate<T> or(Predicate<? super T> other) {
      Objects.requireNonNull(other);
      return (t) -> {
         return this.test(t) || other.test(t);
      };
   }

   static <T> Predicate<T> isEqual(Object targetRef) {
      return null == targetRef ? Objects::isNull : (object) -> {
         return targetRef.equals(object);
      };
   }

   static <T> Predicate<T> not(Predicate<? super T> target) {
      Objects.requireNonNull(target);
      return target.negate();
   }
}

 

예시

마찬가지로 static method 선언하여 인자로 Predicate(함수형 인터페이스) 전달함으로써 다른 동작을 수행할 수 있다

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isPositive = x -> x > 0;

        List<Integer> inputs = Arrays.asList(10, -5, 4, -2, 0, 3);

        System.out.println(filter(inputs, isPositive)); // [10, 4, 3]
        System.out.println(filter(inputs, isPositive.negate())); // [-5, -2, 0]
        System.out.println(filter(inputs, isPositive.or(x -> x == 0))); // [10, 4, 0, 3]
        System.out.println(filter(inputs, isPositive.and(x -> x % 2 == 0))); // [10, 4]
    }

    public static <T> List<T> filter(List<T> inputs, Predicate<T> condition) {
        List<T> result = new ArrayList<>();
        for(T input : inputs) {
            if(condition.test(input)) result.add(input);
        }

        return result;
    }
}

 

Comparator<T>

- int compare(T var1, T var2) : T 타입의 두 인자를 전달받아 비교하여 int 결과값 리턴 

- Comparator가 Comparable 보다 우선 순위가 높음

- Comparable 외에 새로운 정렬 기준이 필요할 때 Comparator를 덮어씌워 사용

@FunctionalInterface
public interface Comparator<T> {
    int compare(T var1, T var2);
    
    // ..
}

 

참고. Comparable 과 Comparator 

https://dev-ljw1126.tistory.com/62

 

[Java]Comparable 과 Comparator 인터페이스

요약 - Comparable 인터페이스 : 객체 스스로에게 부여하는 한 가지 기본 정렬 규칙을 설정하는 것이 목적 - Comparator 인터페이스 : 기본 정렬 규칙과 다른 정렬 기준을 지정하고 싶을 때 사용 Comparable

dev-ljw1126.tistory.com

 

예시

public class Ch4_Comparator {

    static class Student {
        int id;
        String name;

        public Student(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public int getId() {
            return id;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student(2, "홍길동"));
        students.add(new Student(1, "이순신"));
        students.add(new Student(3, "김철수"));
        students.add(new Student(4, "최영희"));
		
        // id 기준 내림차순 정렬
        Comparator<Student> sortByIdDesc = (s1, s2) -> s2.id - s1.id;  
        Collections.sort(students, sortByIdDesc);
        System.out.println(students);

        // name 기준 내림차순 정렬 
        Comparator<Student> sortByNameDesc = (s1, s2) -> s2.name.compareTo(s1.name);
        Collections.sort(students, sortByNameDesc);
        System.out.println(students);
    }
}


[출력 결과]
// id 내림차순
[Student{id=4, name='최영희'}, 
 Student{id=3, name='김철수'}, 
 Student{id=2, name='홍길동'}, 
 Student{id=1, name='이순신'}] 
// name 내림차순
[Student{id=2, name='홍길동'}, 
 Student{id=4, name='최영희'}, 
 Student{id=1, name='이순신'}, 
 Student{id=3, name='김철수'}]

 


참고

 

https://fastcampus.co.kr/dev_red_lsh

 

The Red: 25개 백엔드 개발 필수 현업 예제를 통해 마스터하는 JAVA STREAM | 패스트캠퍼스

글로벌 SNS 기업이자 자바 오픈 소스 분야의 강자인 LinkedIn의 시니어 소프트웨어 엔지니어인 이승환 강사의 강의입니다. 이승환 강사는 백엔드 개발자로 LinkedIn에서 회원관리 및 거래 관리 부분

fastcampus.co.kr

 

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

 

Lambda Expressions (The Java™ Tutorials > Learning the Java Language > Classes and Objects)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 

https://tecoble.techcourse.co.kr/post/2020-07-17-Functional-Interface/

 

 

반응형