1. Java에서 람다
자바에서 함수는 변수에 할당되거나 파라미터로 전달 될 수 없다. [ =2급 객체=2급 시민 = 1급 객체가 아님]
람다는 1급 객체이다.
익명 클래스 (Anonymous Class)
- 이름이 없는 클래스로, 클래스를 선언하면서 동시에 객체를 생성
- 클래스를 상속하거나 인터페이스를 구현할 때 일회성으로 사용
- 한 번만 사용되는 클래스를 위해 별도의 파일을 만들지 않아도 될 때 유용
// 1. Runnable 인터페이스 구현
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Running");
}
};
// 2. 이벤트 리스너
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked!");
}
});
// 3. Comparator 구현
Arrays.sort(array, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
compare()는 익명함수가 아니다.
단순히 Comparator 인터페이스의 추상 메서드를 구현(implement)한 것
이는 일반적인 메서드 오버라이딩이며, 익명 함수와는 다른 개념
comparator 부분에 익명클래스가 아닌 익명함수를 이용하면 다음과 같이 된다.
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
위의 예시에서 Runnable ,ActionListener, Comparator 전부 함수형 인터페이스이다.
즉 함수형 인터페이스를 전달인자로 받는곳에 익명클래스를 정의하거나, 익명함수를 정의해서 전달해줄 수 있다.
익명클래스를 정의하든 익명함수를 정의하든, 결국 해당 함수형 인터페이스를 구현한 익명객체가 전달되는것이다.
- 익명 클래스와 익명 함수(람다 표현식)는 결국 같은 목적을 가집니다. 두 방법 모두 함수형 인터페이스를 구현하는 객체를 전달하기 위해 사용됩니다.
- 익명 클래스는 기존 클래스나 인터페이스를 구현할 때 사용되며, new 키워드를 사용하여 객체를 생성합니다.
- 람다 표현식은 더 간결하고 직관적인 방식으로 함수형 인터페이스를 구현합니다.
- 두 방식 모두 함수형 인터페이스의 구현체로서 사용되므로, 결국 동일한 결과를 낳습니다.
익명 객체 (Anonymous Object)
- Java에서는 순수한 익명 객체 생성 문법은 없음
- 익명 클래스의 인스턴스가 익명 객체가 됨
- 항상 어떤 클래스를 상속하거나 인터페이스를 구현해야 함
위 예시에서 new Comparator ~ 로 생성된 객체가 익명객체
익명 함수 (Anonymous Function)
- 이름이 없는 함수로, Java 8부터 람다 표현식으로 구현
- 주로 함수형 인터페이스를 구현할 때 사용
- 메서드를 변수처럼 다룰 수 있게 해줌
// 1. 기본 람다 표현식
Function<Integer, Integer> square = (x) -> x * x;
// 2. 여러 줄의 람다
Function<Integer, Integer> complex = (x) -> {
int result = x * x;
return result + x;
};
// 3. Consumer 람다
Consumer<String> printer = (s) -> System.out.println(s);
// 4. Comparator 람다
Comparator<String> comparator = (s1, s2) -> s1.compareTo(s2);
comparator.accept(1,2) // 이런식으로 사용
// 인터페이스에 정의된 메소드이름을 이용하여 사용한다.
2. 코틀린에서의 람다
Java와는 근본적으로 다른 한 가지가 있다.
자바에서는 람다를 이용하여 함수를 넘기는것처럼 보여지는 거지 근본적으로는 넘길 수 없었다.
하지만
코틀린에서는 함수가 그 자체로 값이 될 수 있다.
변수에 할당할수도, 파라미터로 넘길 수도 있다.
일반적인 익명함수 구현방법
// 코틀린의 익명함수
//원래는 fun 함수이름(매개변수): 반환값 { 로직 } 이런식인데 익명함수이므로 함수이름이 없다.
val isApple = fun(fruit: Fruit):Boolean {
return fruit.name == "사과"
}
람다를 이용한 방법
// 중괄호와 화살표를 이용하는 익명함수
val isApple2 = {fruit:Fruit -> fruit.name == "사과"}
호출방법
fun main() {
// 일반적 함수 호출
isApple(fruits[0])
// invoke == 호출하다, 언급하다
isApple.invoke(fruits[0])
}
함수의 타입 표기 가능
// isApple 이라는 익명함수는 Fruit를 받아서 Boolean을 반환하는 함수라고 함수타입을 표기할 수 있다.
val isApple:(Fruit) -> Boolean = fun(fruit: Fruit):Boolean {
return fruit.name == "사과"
}
val isApple2:(Fruit) -> Boolean = {fruit:Fruit -> fruit.name == "사과"}
자바에서는 함수형 인터페이스를 파라미터로 지정하고,
익명클래스 또는 익명함수를 이용하여 익명객체를 전달인자로 건내줬는데
코틀린에서는 기본적으로 파라미터에 함수를 받을 수 있다.
[코틀린에서는 함수가 1급객체이다.]
예시)
val isApple:(Fruit) -> Boolean = fun(fruit: Fruit):Boolean {
return fruit.name == "사과"
}
val isApple2:(Fruit) -> Boolean = {fruit:Fruit -> fruit.name == "사과"}
//일반함수
fun isApple3(fruit: Fruit): Boolean {
return fruit.name == "사과"
}
private fun filterFruits(
fruits: List<Fruit>,
funcName: (Fruit) -> Boolean
// 함수 자체를 파라미터로 받을 수 있다. 이름을 지정해줄수있다.
// 파라미터로 받는 함수의 타입은 (Fruit) -> Boolean으로 지정해두었다.
)
: List<Fruit> {
val results = mutableListOf<Fruit>()
for (fruit in fruits) {
if (funcName(fruit)) {
results.add(fruit)
}
}
return results
}
fun main() {
//코틀린에서는 기본적으로 함수도 파라미터로 전달가능
val result = filterFruits(fruits, isApple) // 익명함수 전달가능
val result2 = filterFruits(fruits, isApple2) // 익명함수 전달가능
/*
일반함수도 전달 가능하지만, 함수 참조 방법을 사용해야한다.
왜냐하면 함수타입을 (Fruit) -> Boolean으로 지정해두었기 때문에 현재는 익명함수만 받을 수 있기 때문이다.
일반함수도 ::를 이용하여 함수참조를 진행해야 함수타입으로 변환되므로 ::를 사용해서 전달해준다.
*/
val result3 = filterFruits(fruits, ::isApple3) // 일반함수 전달가능
// 바로 함수 전달가능
val result4 = filterFruits(fruits, {fruit: Fruit -> fruit.name=="사과" })
// 마지막 파라미터가 함수라면 소괄호 밖으로 중괄호를 뺼 수 있다.
// 이렇게하면 중괄호가 앞의 함수의 가장 마지막 파라미터로 들어간다.
val result5 = filterFruits(fruits) { fruit: Fruit -> fruit.name == "사과" }
// 또한 파라미터에 함수타입으로 정의해놨기때문에 함수안의 파라미터 타입이 추론되니까 Fruit 타입 생략가능
val result6 = filterFruits(fruits) { fruit -> fruit.name == "사과" }
// 또한 들어오는 파라미터가 한개라면 it으로 사용할 수 있다.
val result7 = filterFruits(fruits) { it.name == "사과" }
}
3. Closure
이 경우에는 targetFruitName이 바나나였다가 수박으로 바뀌었으므로
람다안에 사용되는 targetFruitName가 final이 아니여서 컴파일 에러가 발생하는것이다.
코틀린에서는 람다가 시작하는 지점에 람다식 안에서 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다.
이렇게 해야만, 람다를 진정한 일급 시민으로 간주할 수 있다. 이 데이터 구조를 Closure라고 부른다.
클로저는 함수나 람다식이 자신이 정의된 환경(변수들)을 "포획"하고, 그 환경을 나중에 사용할 수 있는 데이터 구조를 의미
즉, 람다는 자신이 정의될 당시의 외부 변수의 참조를 기억하고, 그 값을 사용할 수 있는 특성을 가지고 있습니다.
이렇게 포획된 외부 변수는 람다 안에서 참조로 사용되며, 이 변수가 변경될 때마다 최신 상태를 가져오게 됩니다.
즉 람다가 진행되는동안 그 외부변수의 값이 바뀌어도 람다는 최신상태의 외부변수 값을 가져올 수 있다.
이런 데이터 구조를 Closure라고 한다.
람다가 이런 Closure 구조를 가지기 때문에 일급객체로 취급 받는다.
람다가 값으로 다뤄지며, 외부 상태를 캡처하고 동적으로 동작할 수 있다는 특성 덕분에, 변수처럼 다루어지고, 함수처럼 활용되며, 객체처럼 상태를 기억하고 유지할 수 있기 때문입니다.
이러한 특성 덕분에 람다는 단순한 함수가 아니라 동적인 객체처럼 행동하며, 일급 시민으로 취급될 수 있습니다.
4. 다시 try with resources
fun readFile(path: String) {
BufferedReader(FileReader(path)).use {
reader -> println(reader.readLine())
}
}
use 함수에 대해 살펴보면
Closeable 구현체에 대한 확정함수라는 것을 확인할 수 있다.
그리고 inline 함수이다
그리고 파라미터로 함수를 받고 함수타입은 (T) -> R 이며 block이라는 이름을 붙이고 사용한다.
[= 익명함수를 받을 수 있는 파라미터 설정, 일반함수를 전달하려면 ::라는 함수참조를 이용해야한다. ]
그리고 use를 사용하는 부분만 뜯어서 보면
BufferedReader(FileReader(path)).use {
reader -> println(reader.readLine())
}
use 뒤에 { reader -> ~ } 라는 중괄호를 볼 수 있다.
이것은 마지막 파라미터가 함수이기 때문에 소괄호 밖으로 중괄호를 뺀 모습을 확인할 수 있다.
'인프런 > 자바 개발자를 위한 코틀린 입' 카테고리의 다른 글
11) 코틀린에서 컬렉션을 함수형으로 다루는 방법 (0) | 2025.01.10 |
---|---|
9) 코틀린에서 다양한 함수를 다루는 방법 (0) | 2025.01.10 |
8) 코틀린에서 배열과 컬렉션을 다루는 방법 (0) | 2025.01.09 |
7) 코틀린에서 object 키워드, 중첩 클래스, 다양한 클래스를 다루는 방법 (0) | 2025.01.09 |
6) 코틀린에서 접근 제어를 다루는 방법 (0) | 2025.01.09 |
댓글