람다 함수는 Java 8에 추가되었음.
람다식은 매개 변수를 입력하고 값을 반환하는 짧은 코드 블록임. 함수와 비슷하지만, 람다식에는 별도의 이름이 필요 없고(익명 함수), 바로 본문에서 구현될 수 있음.
두 개 이상의 입력이 있는 함수는 최종적으로 1개의 입력만 받는 람다 대수로 단순화 될 수 있음. 이를 커링(Currying)이라고도 함.
익명 함수들을 공통적으로 일급 객체(First Class citizen)라는 특징을 가지고 있음.
일급 객체: 일반적으로 다를 객체들에 적용 가능한 연산을 모두 지원하는 개체를 가르킴. 함수를 값으로 사용할 수도 있으며 매개변수로 전달 및 변수에 대입하기와 같은 연산들이 가능함.
장점
단점
간단한 형태에서는 단일 매개변수와 식을 포함함.
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
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);
}
}
구현해야 할 추상 메소드가 하나만 정의된 인터페이스
//구현해야 할 메소드가 한개이므로 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);
int 값의 인수를 받아들이고 결과를 생성하는 함수
IntFunction intSum = (x) -> x+1;
System.out.println(intSum.apply(1));
// 결과: 2
동일한 유형의 두 피연산자에 대한 연산을 나타내며 피연산자와 동일한 유형의 결과를 생성하는 함수
BinaryOperator stringSum=(x, y)->x+" "+y;
System.out.println(stringSum.apply("Welcome","Heejin blog"));
// 결과: Welcome Heejin blog
그래서 내가 왜 파파고까지 돌려가면서 공부했냐 이놈 때문임.
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)
는 배열의 어떤 요소e1
과e2
에 대해서도ClassCastException
을 던져서는 안됨.이 정렬은 안정적임이 보장되며, 동일한 요소는 정렬의 결과로 정렬되지 않음.
구현 참고: (이 부분은 시간 복잡도 관련 얘기라서 일단 패스)
구현은 입력 배열에서 오름차순과 내림차순의 이점을 동일하게 가지며, 동일한 입력 배열의 다른 부분에서 오름차순과 내림차순의 이점을 얻을 수 있음.
두 개 이상의 정렬된 배열을 병합하는 데 적합함.
배열을 연결하고 결과 배열을 정렬하기만 하면 됨.(대충 구현이 어떤 논문을 이용해 만들어지고 수정된 내용)
대충 내용은 배열을 정렬해준다는 내용임. 그러면 하나의 의문점이 생길거임.
다 좋은데 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]
를 비교하면 됨.Compare(T o1, T o2)
의 리턴값은 양수, 0, 음수로 이루어져 있음e1 > e2
(e2
가 e1
의 왼쪽으로 이동)e1 == e2
e1 < e2
(e1
이 e2
의 오른쪽으로 이동)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) Comparator
과 Comparable
은 용도와 패키지 차이가 있음.
6) Comparator
에서 람다를 사용할 수 있던 것은 sort
가 단일 함수 인터페이스를 타입으로 하는 매개변수(Comparator<? super T> c
)를 가지고 있었기 때문임.
7) Comparator
호출 메소드 Compare(T o1, T o2)
의 리턴값
e1 > e2
e1 == e2
e1 < e2
람다 stream에 대한 정보도 있긴 했는데 나중에 필요할 때 다시 공부해둬야 겠다.
커링에 대해서 찾다보니깐 클로저라는 개념과 비슷한 듯하다. 클로저랑 커링에 대해서도 한번 찾아봐야겠다.
외국 문서도 파파고 쓰면 어느정도는 알아 들을 수 있구나.. 영어로도 많이 검색해봐야 겠다.
너무 신기하다. 분명 나는 람다를 공부하는데 Comparator을 공부하고, 인터페이스를 공부하고, 제네릭까지 다뤄야 할 지경까지 왔다. 진짜 뭘 공부해야 하는지 알게된 건 너무 좋은데 너무 끔찍하다.