Java : Lambda

공부의 기록·2021년 11월 1일
0

Java

목록 보기
14/19
post-thumbnail

Lambda

본 문서는 2021년 12월 21일 에 기록되었습니다.
Essentials of JavaAPI 문서 와 기타 References 를 참고하였습니다.

함수형 프로그래밍이 뭔지 모른다면, 관련 내용을 구글링 하거나 PP vs OOP vs FP 를 보시길 바랍니다.
또한 인터페이스지네릭스 등에 대한 이해도가 필요합니다.
또한 1등 시민으로서의 함수 에 대한 내용을 선결하여 가볍게 읽고 오시는 것을 추천드립니다.

람다 사용 시에 대한 주의사항도 읽어보시는 것을 추천드립니다.
for-loop 를 Stream.forEach() 로 바꾸지 말아야 할 3가지 이유

구성

이 내용은 다음과 같은 구성으로 이루어져 있습니다.

  1. 서론
    1.0. Function 이란?
    1.1. Functional Interface 이란?

  2. Lamda Expression

  3. Functional Interface
    3.1. Function / BiFunction / TriFunction(custom)
    3.2. Supplier
    3.3. Consumer / BiConsumer
    3.4. Predicate
    3.5. UnaryOperator

  4. Practice


서론

서론은 다음의 내용으로 구성되어 있습니다.

  1. Function 이란?
  2. Functional Interface 이란?
  3. Method Reference 란?

가볍게 읽어보고 API 문서를 참고하여 간단한 코드 예제를 실천하고
이후 Practice 를 통해 복잡함 문제를 풀어봅시다.

Function 이란?

함수란?
어떠한 기능을 달성하기 위한 단위의 일환.

함수의 구성요소는 다음과 같습니다.

  1. 함수명
  2. 반환값 타입
  3. 매개변수 타입
  4. 함수필드(함수내용)

Functional Interface 란?

Functional Interface 는 기본적으로
단 하나의 abstract method 를 가지는 인터페이스 를 의미합니다.

Lamda 식과 같이 사용하며 코드의 간결함이라는 장점을 가지게 됩니다.

Method Reference 란?

Method Reference 는
기존에 선언되어 있는 메서드를 지정하는 기능 을 의미합니다.

FI + Lamda 보다 더 많은 생략이 이루어지기 때문에,
Method Reference 를 사용하기 위해서는 사용할 메서드의 매개변수 및 리턴 타입을 숙지해야 합니다.


Lamda Expression

아주 짧고 간단하게 Lamda Expression 을 알아보겠습니다.
이것은 다음과 같은 구조로 생겼습니다.

(Integer x)->{
   System.out.print(x);
}
(Integer x)->{
   return x+3;
}

메서드와 형태만 다를 뿐 완벽하게 동일합니다.

추가적으로 알면 좋은 부분들은 다음과 같습니다.

  1. 타입 생략 | 타입을 예상할 수 있는 경우에 생략이 가능합니다.
  2. 타입+이름 을 감싸는 괄호 생략 | 매개변수가 1개 뿐이라면 생략이 가능합니다.
  3. 프로세스 를 감싸는 괄호 생략 | 프로세스에 코드 한 줄만 있다면 생략이 가능합니다.
x->System.out.print(x);
x->x+3;

Functional Interface

Functional Interface 는 매우 많습니다.
하지만 이 중에서도 뿌리가 되거나 주요한 인터페이스 몇 개 를 알아보도록 하였습니다.

알아볼 Functional Interface 는 다음과 같습니다.

  1. Function / BiFunction / TriFunction(custom)
  2. Supplier
  3. Consumer / BiConsumer
  4. Predicate
  5. UnaryOperator

또한 사칙연산이나 배열과 연계된 출력문 등의 간단한 예제를 들어서 설명할 것입니다.
이 경우에 용어가 너무 긴 관계로 FI + Lamda 라고 표기하였습니다.

Function

매개변수 T와 리턴값 R

@FunctionalInterface
public interface Function <T, R> {
   R apply(T t);
}

에제 1 | t 를 입력받는 FI + Lamda

만들어보자!
Function <Integer, Integer> process1=(Integer number)->{
   return number+10;
};
Function <Integer, Integer> process2=(Integer number)->{
   return number*5;
};
Function <Integer, Integer> process3=(Integer number)->{
   return number/3;
}
생략해보자!
Function <Integer, Integer> process1=number->number+10;
Function <Integer, Integer> process2=number->number*5;
Function <Integer, Integer> process3=number->number/3;
사용해보자!
System.out.print(process1.apply(50)); // 60
System.out.print(process2.apply(50)); // 250
System.out.print(process3.apply(50)); // 16

BiFunction

매개변수 T,U와 리턴값 R

@FunctionalInterface
public interface BiFunctioanl<T, U, R> {
   R apply(T t, U u);
}
만들고 생략해보자!
BiFunction<Integer, Integer, Integer> process1=(number1, numeber2)=> number1+number2;
   
BiFunction<Integer, Integer, Integer> process2=(number1, numeber2)=> number1-number2;
   
BiFunction<Integer, Integer, Integer> process3=(number1, numeber2)=> number1*number2;
   
BiFunction<Integer, Integer, Integer> process4=(number1, numeber2)=>
   (number2==0) number1 : number1/number2;
사용해보자!
System.out.print(process1.apply(10,10); // 20
System.out.print(process2.apply(10,10); // 0
System.out.print(process3.apply(10,10); // 100
System.out.print(process4.apply(10,0); // 10

TriFunction (custom)

매개변수 T,U,V와 리턴값 R

@FunctionalInterface
public interface TriFunction<T, U, V, R> {
	R apply(T t, V v, R r);
}
만들고 생략해보자!
TriFunction<Integer, Integer, Integer, Integer> process=(x,y,z)->x+y+z;
사용해보자!
System.out.println(process.apply(100,10,1));

Supplier

리턴값 T

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
만들고 생략해보자!
Supplier<Integer> process=()->10;
Supplier<String> process=()->"하이";
사용해보자!
System.out.print(process.get());

Consumer

매개변수 T

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}
만들고 생략해보자!
Consumer<Integer> process=number->System.out.print(number);
사용해보자!
process.accept(10); // 10
process.accept(30); // 30
대체 왜?

2021년 12월 21일 현재 아직도 왜 쓰는지 모르겠지만,
그나마 쓸만한 경우를 찾아보면 다음 정도 밖에 없지 않을까 싶다.

publci class Main {
   public static void main(String[] args) {
      List<Integer datas=new ArrayList<>(Arrays.asList(10,20,30,40,50));
      Consumer<Integer> process=data->System.out.print(data);
   }
   
   public static void process(List<Integer> datas, Consumer<Integer> consumer) {
      for(Integer data : datas) {
         consumer.accept(data);
      }
   }
}

BiConsumer

매개변수 T,U

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);

    default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
        Objects.requireNonNull(after);

        return (l, r) -> {
            accept(l, r);
            after.accept(l, r);
        };
    }
}
만들고 생략해보자!
BiConsumer<Integer, String> process=(name, age)->
   System.out.printf("%s 이의 나이는 %d 입니다.%n",name,age);
사용해보자
process.accept("unchaptered", 30);
대체 왜?
public class Main {
   public static void main(String[] args){
      List<String> datas=new ArrayList<>(Arrays.asList("김팔순","김덕해","홍영수"));
      BiConsumer<Integer, String> process=(index,name)->
         System.out.printf("%d 번 사용자의 이름은 %s 입니다.%n",index,name);
   }
   public static void process(List<String> datas, BiConsumer<Integer, String> process) {
      for(int i=0; i<datas.size(); i++){
         process.accept(i, list.get(i));
      }
   }
}

Predicate

매개변수 T와 리턴값 boolean

public interface Predicate<T> {
    boolean test(T t);

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

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

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

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}
대체 왜?
public class Main {
	public static void main(String[] args) {
		Predicate<Integer> isPositive=x->x>0;
		List<Integer> list=new ArrayList<>(Arrays.asList(10, 3, -5, 4, -2 , 0));
		System.out.println("기본 | "+list);
		System.out.println();
		System.out.println(filter(list,isPositive)); // 10, 4
		System.out.println(filter(list,isPositive.negate())); // -5, -2, 0
		System.out.println(filter(list,isPositive.or(x->x==0))); // 10, 4, 0
		System.out.println(filter(list,isPositive.and(x->x%2==0))); // 10, 4
		
		System.out.println(filter(list,isPositive.and(x->x%2==0 && x%3!=0))); // 10, 4
		System.out.println(filter(list,isPositive.and(x->x%2==0).and(x->x%3!=0))); //10,4
	}
	
	public static <T> List<T> filter(List<T> list, Predicate<T> condition) {
		ArrayList<T> output=new ArrayList<>();
		for(T element : list) {
			if(condition.test(element)) {
				output.add(element);
			}
		}
		output.trimToSize();
		return output; // 오토 업캐스팅 이루어짐
	}
}

UnaryOperator

이 인터페이스는 Function을 상속받으며 매개변수와 리턴값이 동일하면 하나의 타입 매개변수만을 사용한다 라는 개념이다.

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

Comparator

@FunctionalInterface
public interface Comparator<T> {
   int compare(T o1, T o2);
}

부연설명 | Comparator 는 비교를 위한 인터페이스입니다.
단순한 값의 비교 뿐만 아니라, 객체를 비교하기 위해서도 사용할 수 있습니다.

  1. 음수면 o1 "<" o2
  2. 음수면 o1 "=" o2
  3. 양수면 o1 ">" o2
User 클래스
public class User {
   private int id;
   private String name;
   
   public User(int id, String name) {
      this.id = id;
      this.name = name;
   }

   public int getId() { /* 생략 */ } }
   public String getName() { /* 생략 */ }

   @Override public int hashCode() { /* 생략*/ } }
   @Override public boolean equals(Object obj) { /* 생략 */ }

   @Override public String toString() {
      return "User [id=" + id + ", name=" + name + "]";
   }
}
사용 및 결과 보기
public class UserTest {
   static void main(String[] args) {
      List<User> userList=new ArrayList<>(10);
		
      userList.add(new User(3,"ceil"));
      userList.add(new User(1,"bellian"));
      userList.add(new User(5,"angelica"));
      System.out.println(userList+"\n");
		
      Comparator<User> idComparator=(u1, u2)->u1.getId() - u2.getId();
      Collections.sort(userList,idComparator);
      System.out.println(userList+"\n");
		
      Comparator<User> nameComparator=(u1, u2)->u1.getName().compareTo(u2.getName());
      Collections.sort(userList,nameComparator);
      System.out.println(userList+"\n");
   }
}


Method Reference

람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다.
그런데 자바에는 함수 객체를 심지어 람다 보다도 간결하게 만드는 방법이 있으니, 바로 메서드 참조이다. ( :: 연산자를 사용 )
Effective Java 6 | Lamda, Stream

여기서는 다음과 같은 내용을 다루고 있습니다.

  1. ClassName::staticMethodName
  2. objectName::instanceMethodName
  3. ClassName::instanceMethodName
  4. ClassName::new

ClassName::staticMethodName

Function<String, Integer> strToint=Integer::parseInt;
int number=strToint.apply("403");
System.out.print(number); // 403

objectName::instanceMethodName

String textA="hello";                     
String textB="world";                     
Predicate<String> equalsStrToStr=textA::equals
boolean bools=equalsStrToStr.test("hello"); // true
boolean bools2=equalsStrToStr.test(textA); // true
boolean bools3=equalsStrToStr.test(textB); // false

확장

FI + Lamda 사용과 Lazy Evaluation

Supplier 사용 시 성능 향상 이유 + Lazy Evaluation
아마 위 사용예시만 보고는 사용 이유를 체감할 수 없을 거라고 생각한다.
그러나 만약에 Supplier 가 어떠한 연산을 담당하고 있다면 이야기가 달라진다.

일반함수를 매개변수로 넘겨주는 경우 즉시 실행 한다.
하지만, Supplier 는 get() 을 통해 실행시점을 결정 할 수 있게 된다.

이러한 것을 활용할 수 있는 것이 바로 Lazy Evaluation 이라는 개념이다.

Lazy Evaluation

[Javascript] 지연 평가(Lazy evaluation) 를 이용한 성능 개선
우리는 @Functional Interface 를 활용하여 특정 기능의 실행 시점을 결정할 수 있게 되었다.

이 부분이 이해가 되지 않는 사람이 있다면 아래를 확인해보자.

hello() // 기존의 메서드 방식
hello.get() // Functional Interface 에서 추구하는 방식

일반 메서드에서는 이름() 형태로써 실행 시점을 결정하기 힘들었다.
하지만 @Functional Interface 에서는 이름.실행함수() 의 형태로써 매개변수화 및 실행호출의 분리라는 큰 장점을 가져오게 되었다.

이를 통해서 Strict Evaluation 과 Lazy Evaluation 을 구현할 수 있다.
위 링크를 타고 들어가면 나오는 언어는 JavaScript 를 기반으로 하고 있다.

Java 기반으로 된 설명을 찾아보니 제대로된 펙트를 이야기하는 글을 찾기 힘들고 위 포스트가 더 깔끔하고 정확하여 추가하게 되었다.

const arr = [0, 1, 2, 3, 4, 5]
const result = arr.map(num => num + 10).filter(num => num % 2).slice(0, 2)
console.log(result) // [11, 13]
// 지연평가를 하기 위해서는 지연평가 용 함수(Java 의 Functional Interface) 를 만들어야 한다.
const arr = [0, 1, 2, 3, 4, 5]
const result = _.take(2, L.filter(num => num % 2, L.map(num => num + 10, arr)));
console.log(result) // [11, 13]

해당 내용에 대한 자세한 설명은 아래 포스트를 참고하는 것이 더 낫다고 본다.
[JS] ES6 를 이용한 함수형 프로그래밍 - 12 take

profile
2022년 12월 9일 부터 노션 페이지에서 작성을 이어가고 있습니다.

0개의 댓글