인프런/자바 개발자를 위한 코틀린 입

10) 코틀린에서 람다를 다루는 방법

backend dev 2025. 1. 10.

1. Java에서 람다

 

 

코드 가독성 높이는 자바 람다식과 함수형 인터페이스 | 요즘IT

자바(Java)는 시간이 지남에 따라 발전하면서 더욱 효율적이고 간결한 코드를 작성할 수 있도록 다양한 기능을 제공하고 있습니다. 특히 자바 8부터 도입된 람다식과 함수형 인터페이스는 자바에

yozm.wishket.com

 

[Java] Predicate란?

🤔 서론 우테코 오늘자 강의에서 BiPredicate라는 개념을 처음 들어보았다. Predicate란 무엇인지, 언제 사용하는 것인지 그리고 내 코드에 적용하는 과정까지를 담아본다. 생각보다 어렵지 않다. 설

yeonyeon.tistory.com

 

 

👨‍💻 일급 객체(first-class object) 란?

일급 객체 란? 보통 자바의 람다 표현식(Lambda Expression)을 배우다 보면 '일급 객체' 라는 단어를 접하게 되는데, 뜻을 아무리 봐도 대체 무얼 말하는 건지 와닿지 않을 것이다. '일급' 이란 뜻은 일

inpa.tistory.com

 

 

 

Java - 메소드 레퍼런스(Method Reference) 이해하기

메소드 레퍼런스(Method Reference)는 Lambda 표현식을 더 간단하게 표현하는 방법입니다. 메소드 레퍼런스는 사용하는 패턴에 따라 다음과 같이 분류할 수 있습니다. Static 메소드 레퍼런스, Instance 메

codechacha.com

 

자바에서 함수는 변수에 할당되거나 파라미터로 전달 될 수 없다. [ =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 -> ~ } 라는 중괄호를 볼 수 있다.

 

이것은 마지막 파라미터가 함수이기 때문에 소괄호 밖으로 중괄호를 뺀 모습을 확인할 수 있다. 

 

 

댓글