[Java] Lambda와 [백준 11650] 좌표 정렬하기

6720·2023년 1월 6일
0

이거 모르겠어요

목록 보기
1/38
post-custom-banner

Java Lambda Expressions

람다 함수는 Java 8에 추가되었음.

람다식은 매개 변수를 입력하고 값을 반환하는 짧은 코드 블록임. 함수와 비슷하지만, 람다식에는 별도의 이름이 필요 없고(익명 함수), 바로 본문에서 구현될 수 있음.

  • 두 개 이상의 입력이 있는 함수는 최종적으로 1개의 입력만 받는 람다 대수로 단순화 될 수 있음. 이를 커링(Currying)이라고도 함.

  • 익명 함수들을 공통적으로 일급 객체(First Class citizen)라는 특징을 가지고 있음.
    일급 객체: 일반적으로 다를 객체들에 적용 가능한 연산을 모두 지원하는 개체를 가르킴. 함수를 값으로 사용할 수도 있으며 매개변수로 전달 및 변수에 대입하기와 같은 연산들이 가능함.

장단점

장점

  • 코드의 간결성: 불필요한 반복문 삭제 가능, 복잡한 식을 단순하게 표현할 수 있음.
  • 지연연산 수행: 불필요한 연산 최소화
  • 병렬처리 가능: 멀티스레드를 활용하여 병렬처리 사용 가능함.

단점

  • 람다식의 호출이 까다로움
  • 람다 stream 사용 시 단순 for문이나 while문의 성능이 떨어짐.
  • 불필요하게 사용된다면 가독성을 떨어뜨림.

Syntax

간단한 형태에서는 단일 매개변수와 식을 포함함.

parameter -> expression

하나 더 많은 매개변수를 사용할 때는, 매개변수들을 괄호로 묶음*.
*(wrap them in parentheses -> 그것들을 괄호로 싸다)

(parameter1, parameter2) -> expression

(표현)식은 제한되어 있음. 값을 즉시 반환해야 하고 변수, 인자 또는 if와 for과 같은 문을 포함할 수 없음.

(parameter1, parameter2) -> { code block }

식이 있는 자리를 함수몸체라고도 하는데, 함수몸체가 단일 실행문이면 괄호{} 생략 가능
단, 함수몸체가 return문으로만 구성되어 있는 경우는 괄호{} 생략 불가능

// 나머지 기타 유형
() -> {}
() -> 1
() -> { return 1; }

(int x) -> x+1
(x) -> x+1
x -> x+1
(int x) -> { return x+1; }
x -> { return x+1; }

(int x, int y) -> x+y
(x, y) -> x+y
(x, y) -> { return x+y; }

(String lam) -> lam.length()
lam -> lam.length()
(Thread lamT) -> { lamT.start(); }
lamT -> { lamT.start(); }

// 잘못된 유형 선언된 type과 선언되지 않은 type을 같이 사용 할 수 없다.
// 선언해줄려면 다 해주거나 아예 하지 말아라
(x, int y) -> x+y
(x, final y) -> x+y

Using Lambda Expressions

0) 이런 느낌

new Thread(new Runnable() {
   @Override
   public void run() { 
      System.out.println("Welcome Heejin blog"); 
   }
}).start();
new Thread(()->{
      System.out.println("Welcome Heejin blog");
}).start();

1) 람다식은 보통 함수에 매개변수로 전달됨.

import java.util.ArrayList;

public class Main {
  public static void main(String[] args) {
    ArrayList<Integer> numbers = new ArrayList<Integer>();
    numbers.add(5);
    numbers.add(9);
    numbers.add(8);
    numbers.add(1);
    numbers.forEach( (n) -> { System.out.println(n); } );
  }
} // result: 5(\n)9(\n)8(\n)1(\n)

forEach(): List의 모든 아이템을 순회함.

numbers.forEach(new Consumer<String>() {
    @Override
    public void accept(String n) {
        System.out.println(n);
    }
});

원래 forEach는 함수형 인터페이스를 인자로 받기 때문에 람다식을 사용하면 간편하게 사용이 가능함.

2) 변수 유형이 함수가 하나만 있는 인터페이스의 경우 람다식을 변수에 저장할 수 있음.

대신 해당 방식과 동일한 수의 모수와 동일한 반환 타입을 가져야 함.

import java.util.ArrayList;
import java.util.function.Consumer;

public class Main {
  public static void main(String[] args) {
    ArrayList<Integer> numbers = new ArrayList<Integer>();
    numbers.add(5);
    numbers.add(9);
    numbers.add(8);
    numbers.add(1);
    Consumer<Integer> method = (n) -> { System.out.println(n); };
    numbers.forEach( method );
  }
}

3) 함수에서 람다식을 사용하려면 함수는 단일 함수 인터페이스를 타입으로 하는 매개변수가 있어야 함.

인터페이스 함수를 호출하면 람다식을 실행함.

interface StringFunction {
  String run(String str);
}

public class Main {
  public static void main(String[] args) {
    StringFunction exclaim = (s) -> s + "!";
    StringFunction ask = (s) -> s + "?";
    printFormatted("Hello", exclaim);
    printFormatted("Hello", ask);
  }
  public static void printFormatted(String str, StringFunction format) {
    String result = format.run(str);
    System.out.println(result);
  }
}

함수형 인터페이스

@FunctionalInterface

구현해야 할 추상 메소드가 하나만 정의된 인터페이스

//구현해야 할 메소드가 한개이므로 Functional Interface이다.
@FunctionalInterface
public interface Math {
    public int Calc(int first, int second);
}

//구현해야 할 메소드가 두개이므로 Functional Interface가 아니다. (오류 사항)
@FunctionalInterface
public interface Math {
    public int Calc(int first, int second);
    public int Calc2(int first, int second);

java.util.function 인터페이스

IntFunction<R>

int 값의 인수를 받아들이고 결과를 생성하는 함수

IntFunction intSum = (x) -> x+1;
System.out.println(intSum.apply(1));
// 결과: 2

BinaryOperator<T>

동일한 유형의 두 피연산자에 대한 연산을 나타내며 피연산자와 동일한 유형의 결과를 생성하는 함수

BinaryOperator stringSum=(x, y)->x+" "+y;
System.out.println(stringSum.apply("Welcome","Heejin blog"));
// 결과: Welcome Heejin blog

다른 인터페이스 목록

이 글의 목적(백준 11650 좌표 정렬하기)

그래서 내가 왜 파파고까지 돌려가면서 공부했냐 이놈 때문임.

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int n = Integer.parseInt(br.readLine());
        int[][] coordinate = new int[n][2];
        int result = 0;
        for (int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            coordinate[i][0] = Integer.parseInt(st.nextToken());
            coordinate[i][1] = Integer.parseInt(st.nextToken());
        }
//=======================[주목]=======================
        Arrays.sort(coordinate, (e1, e2) -> {
            if (e1[0] == e2[0]) {
                return e1[1] - e2[1];
            } else {
                return e1[0] - e2[0];
            }
        });
//====================================================

        bw.flush();
        br.close();
        bw.close();
    }
}
Arrays.sort(coordinate, (e1, e2) -> {
	if (e1[0] == e2[0]) {
		return e1[1] - e2[1];
	} else {
		return e1[0] - e2[0];
	}
});

이 부분은 배열을 내림차순 정렬하는 부분임. 2차원 배열에는 x, y 좌표가 저장되고 x 좌표를 비교해서 작은 순으로 출력하는 것임. (x 좌표가 같다면 y 좌표가 작은 순으로 출력됨.)

이 부분에서 람다가 사용됐다면 Array.sort()를 다시 살펴볼 필요가 있음.

sort에는 많은 케이스의 매개변수가 들어가는데 여기에서 Comparator<? super T> c를 매개변수로 사용하는 케이스를 찾아야 함.

sort
public static <T\> void sort(T\[] a, Comparator<? super T> c)
지정한 비교기에서 유도한 순서에 따라 지정한 개체 배열을 정렬함.
배열의 모든 요소는 지정된 비교기에 의해 상호 비교가 가능해야 함.
즉, c.compare(e1, e2)는 배열의 어떤 요소 e1e2에 대해서도 ClassCastException을 던져서는 안됨.

이 정렬은 안정적임이 보장되며, 동일한 요소는 정렬의 결과로 정렬되지 않음.

구현 참고: (이 부분은 시간 복잡도 관련 얘기라서 일단 패스)

구현은 입력 배열에서 오름차순과 내림차순의 이점을 동일하게 가지며, 동일한 입력 배열의 다른 부분에서 오름차순과 내림차순의 이점을 얻을 수 있음.
두 개 이상의 정렬된 배열을 병합하는 데 적합함.
배열을 연결하고 결과 배열을 정렬하기만 하면 됨.

(대충 구현이 어떤 논문을 이용해 만들어지고 수정된 내용)

대충 내용은 배열을 정렬해준다는 내용임. 그러면 하나의 의문점이 생길거임.
다 좋은데 Comparator가 왜 lambda랑 연관이 있는거임?

Comparator에 대한 설명은 여기에 정리해뒀음.

Lambda와 Comparator

우선 위에 설명에서 봤듯이 comparator은 두 매개변수 객체를 비교하는 것임. 결국 비교하게 되는 것은 2차원 배열의 \[i]\[0]\[j]\[0] 이런 식으로 비교하게 될 것임.

다시 sort 설명했던 걸 떠올려서 형태를 살펴봐야 함.
public static <T\> void sort(T\[] a, Comparator<? super T> c)

sort는 두 가지 인자를 받는데, 그 중 하나가 제너릭 타입 객체배열임.
제너릭도 나중에 다루겠지만 클래스, 오브젝트, 자료형 등 어떤 타입이든 상관없이 배열의 형태로 받을 수 있다는 뜻임. 여기서의 T가 카멜레온 역할을 한다고 생각하면 될듯.

그렇다면 여기에 int\[]\[]를 넣게 되면 T는 어떻게 될까?
결론부터 얘기하자면 T = int\[]가 될 것임.
int\[]가 하나의 타입이고, int\[] 타입의 배열형태인 int\[]\[]라는 1차원 배열이라고 볼 수도 있음. (다만 이건 의미상으로 그렇다는 것임 - 이런 뉘앙스로 알아서 잘 생각하면 됨.)

sort가 받는 마지막 인자는 Comparator<? super T> c 인데 <? super T>는 상속관계에 있는 타입만 자료형으로 받겠다는 의미임.
즉, T 타입이 자식클래스가 되고, T의 상속관계에 있는 타입까지만 허용하겠다는 의미임.
물론 이 문제에서는 상속관계에 있는 데이터를 정렬하지 않기 때문에 이를 생략하여 <T>라고 봐도 됨.

그래서 왜? comparator은 람다로 표현할 수 있을까? 위에 Using Lambda Expressions에서 3번 설명을 다시 보면 함수에서 람다식을 사용하려면 함수는 단일 함수 인터페이스를 타입으로 하는 매개변수가 있어야 함인데 sort의 경우 Comparator라는 단일 함수 인터페이스가 있음. 그래서 Comparable도 안되는게 Comparator에서는 가능한 것.

문제 풀이

Arrays.sort(arr, new Comparator<int[]>() {		
	@Override
	public int compare(int[] e1, int[] e2) {
		if(e1[0] == e2[0]) {
			return e1[1] - e2[1];
		}
		else {
			return e1[0] - e2[0];
		}
	}
});

만약 람다를 사용하지 않고 Comparator을 그대로 사용했으면 이렇게 됐을 것임.
우선 알아둬야 할 건 뭐냐면

  • T = int\[]라는 것
    결국 비교를 해야할 부분은 arr[i][0]arr[i+1][0]을 비교하고 두 값이 같다면 arr[i][1]arr[i+1][1]을 비교해야 할 것. -> coordinate(배열)의 [1] (coordinate[i][1])을 비교하기 위해 사용자화 해야함.
  • 값이 e1, e2인데 각각 하나의 좌표를 나타내고 있음. -> 그렇기 때문에 x좌표를 비교할 때 x좌표가 저장된 e1[0]e2[0]를 비교하면 됨.
  • 람다, 즉 Comparator 호출 메소드 Compare(T o1, T o2)의 리턴값은 양수, 0, 음수로 이루어져 있음
    [e1 - e2 반환 값]
    • 양수: e1 > e2 (e2e1의 왼쪽으로 이동)
    • 0: e1 == e2
    • 음수: e1 < e2 (e1e2의 오른쪽으로 이동)
Arrays.sort(coordinate, (e1, e2) -> {
	if (e1[0] == e2[0]) {
		return e1[1] - e2[1];
	} else {
		return e1[0] - e2[0];
	}
});

결론

1) 람다는 익명 함수여서 바로 본문에서 구현 가능함.
2) 여러모로 편하지만 가독성이 떨어질 수 있어 사용에 주의해야 함.
3) 기본 형태는 (parameter1, parameter2) -> { code block }
4) 람다 사용 방식

  • 보통 함수에 매개변수로 전달됨.
  • 변수 유형이 함수가 하나만 인터페이스의 경우 람다식을 변수에 저장할 수 있음.
  • 함수에서 람다식을 사용하려면 함수는 단일 함수 인터페이스를 타입으로 하는 매개변수가 있어야 함.

5) ComparatorComparable은 용도와 패키지 차이가 있음.
6) Comparator에서 람다를 사용할 수 있던 것은 sort가 단일 함수 인터페이스를 타입으로 하는 매개변수(Comparator<? super T> c)를 가지고 있었기 때문임.

7) Comparator 호출 메소드 Compare(T o1, T o2)의 리턴값

  • 양수: e1 > e2
  • 0: e1 == e2
  • 음수: e1 < e2

후기

람다 stream에 대한 정보도 있긴 했는데 나중에 필요할 때 다시 공부해둬야 겠다.

커링에 대해서 찾다보니깐 클로저라는 개념과 비슷한 듯하다. 클로저랑 커링에 대해서도 한번 찾아봐야겠다.

외국 문서도 파파고 쓰면 어느정도는 알아 들을 수 있구나.. 영어로도 많이 검색해봐야 겠다.

너무 신기하다. 분명 나는 람다를 공부하는데 Comparator을 공부하고, 인터페이스를 공부하고, 제네릭까지 다뤄야 할 지경까지 왔다. 진짜 뭘 공부해야 하는지 알게된 건 너무 좋은데 너무 끔찍하다.

참고 링크

profile
뭐라도 하자
post-custom-banner

0개의 댓글