Notice
Recent Posts
Recent Comments
Link
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags more
Archives
Today
Total
관리 메뉴

프로그래밍 공부

[자바] #18. 메서드 참조(+ 람다식에서 접근 가능한 참조변수 제한) 본문

자바

[자바] #18. 메서드 참조(+ 람다식에서 접근 가능한 참조변수 제한)

하 냥 2025. 2. 20. 12:43

🔍 메서드 참조란?

 

메서드 참조(Method Reference)는 람다 표현식을 더 간결하게 표현하는 방법이다.

메서드 참조를 사용하면 기존 메서드 이름을 그대로 참조하여 람다식을 대체할 수 있다.

 

람다식 vs 메서드 참조 비교

💡 람다식

list.forEach(s -> System.out.println(s));

 

💡 메서드 참조

list.forEach(System.out::println);

두 코드는 동일한 동작을 수행한다.

메서드 참조람다식이 단순히 메서드를 호출하는 경우 사용된다!


🏷️ 메서드 참조의 종류와 예제

타입 문법 예제 설명
static 메서드 참조 ClassName::staticMethod Math::max 클래스의 static 메서드를 참조
인스턴스 메서드 참조 instance::instanceMethod System.out::println 특정 객체의 인스턴스 메서드를 참조
클래스 이름을 통한
인스턴스 메서드 참조
ClassName::instanceMethod String::toUpperCase 특정 클래스에 있는
인스턴스 메서드를 참조
생성자 참조 ClassName::new ArrayList::new 클래스의 생성자를 참조

 


📜 1) static 메서드 참조 (ClassName::staticMethod)

예제: Math 클래스의 max() 메서드 참조

import java.util.function.BiFunction;

public class StaticMethodReferenceExample {
    public static void main(String[] args) {
    	// BiFunction<T, U, R>, R apply (T t, U u) 구현
        BiFunction<Integer, Integer, Integer> maxFunction = Math::max;
        System.out.println("더 큰 값: " + maxFunction.apply(10, 20));  // 출력: 20
    }
}

 

Math::max는 Math.max(a, b) 메서드를 참조한다.

(BiFunction<T, U, R>는 두 개의 입력값을 받아 하나의 결과를 반환하는 함수형 인터페이스)

 

 

추가 예제:

import java.util.*;
import java.util.function.Consumer;

public class StaticMethodReferenceExample2 {
    public static void main(String[] args) {
    	List<Integer> list = Arrays.asList(1, 3, 5, 7, 9);
        list = new ArrayList<>(list);
        
        Consumer<List<Integer>> c = Collections::reverse; // 메서드 참조문
        c.accept(list);
        System.out.println(list);
    }
}

위 예제에서 메서드 참조문은,

Consumer<List<Integer>> c = list -> Collections.reverse(list);

        ->     Consumer<List<Integer>> c = Collections::reverse;

람다문을 이렇게 바꾼 것이다.

 

위의 메서드 참조에서, 람다식에는 있는 인자 전달에 관한 정보를 생략할 수 있는 이유는 다음 약속에 근거를 둔다.

"accept 메서드 호출 시 전달되는 인자를(이 예제에서는 list), reverse 메서드를 호출하면서 그대로 전달한다"

이러한 약속이 없다면 메서드 참조라는 것이 존재할 수 없다.

그래서 c.accept(list); 에서 전달인자 list를 reverse 메서드에 그대로 전달하는 것이다.


📜 2) 인스턴스 메서드 참조 (instance::instanceMethod)

예제: System.out::println 참조

import java.util.*;

public class InstanceMethodReferenceExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        names.forEach(System.out::println);  // 출력: Alice Bob Charlie
    }
}

System.out::println은 System.out 객체의 println 메서드를 참조한다.

람다식 대체: names.forEach(name -> System.out.println(name));

 

 

추가 예제:

import java.util.*;
import java.util.function.Consumer;

class JustSort {
	public void sort(List<?> list) {
    	Collections.reverse(list);
    }
}

public class InstanceMethodReferenceExample2 {
    public static void main(String[] args) {
    	List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 5, 7, 9));
        JustSort js = new JustSort();
        
        Consumer<List<Integer>> c = js::sort; // 메서드 참조문
        c.accept(list);
        System.out.println(list);
    }
}

위 예제에서 메서드 참조문은,

Consumer<List<Integer>> c = e -> js.sort(e);

    ->   Consumer<List<Integer>> c = js::sort;

람다문을 이렇게 바꾼 것이다.


위 예제에서의 람다식에선, '같은 지역 내에 선언된 참조변수 js에 접근하고 있다'는 독특한 점을 확인할 수 있다.

람다식에서는 같은 지역에 선언된 참조변수에 접근하는 것은 가능한 일인데,

'람다식에서 접근 가능한 참조변수는 final로 선언되었거나 effectively final이어야 한다'

라는 조건을 만족해야만 한다.

 

final로 선언된 변수는 알겠는데, effectively final 변수는 뭘까?

❕ 변수를 final로 명시적으로 선언하지 않았지만, 초기화 후 값이 변경되지 않는 경우이다.
즉, 컴파일러가 내부적으로 final처럼 취급할 수 있으면 effectively final이라고 한다.

public class Example {
    public static void main(String[] args) {
        int number = 10;  // final 키워드는 없지만 값이 변경되지 않음
        Runnable r = () -> System.out.println(number); // 접근 가능
        r.run();
    }
}

여기서 number는 명시적으로 final로 선언되진 않았지만 값이 변경되지 않았기 때문에 effectively final로 간주된다.

따라서 람다식에서 접근할 수 있다.

 

만약 값이 변경될 경우 (컴파일 오류 발생)

public class Example {
    public static void main(String[] args) {
        int number = 10;
        // number = 20; // ❌ 컴파일 오류 발생: 람다식 외부에서 변경하면 안됨
        Runnable r = () -> {
            // number = 20;  // ❌ 컴파일 오류 발생: 람다에서 변경할 수 없음
            System.out.println(number);
        };
        r.run();
    }
}

람다식 외부 및 내부에서 참조변수의 값을 변경하면 안 된다.

number가 초기화 후 다시 변경되었기 때문에, 이 변수는 effectively final 상태를 잃게 된다.

자바 컴파일러는 람다식에서 사용되는 지역 변수가 "값이 한 번만 설정되고 변경되지 않아야 한다"는 규칙을 적용!

따라서, 위 코드에서는 number를 effectively final로 간주할 수 없으므로 컴파일 오류가 발생한다.

 

💡 왜 이런 제한이 있을까?

람다는 본질적으로 익명 클래스와 유사한 방식으로 동작한다.

람다 표현식이 생성될 때, 사용된 변수들은 람다가 생성될 당시의 값을 복사하거나 참조하기 때문에, 값이 변경되면 예상치 못한 상황이 발생할 수 있다.

이를 방지하기 위해 final 또는 effectively final 변수만 사용하도록 제한한 것이다.

public class LambdaExample {
    public static void main(String[] args) {
        String message = "Hello, World!";

        // 익명 클래스 사용
        Runnable anonymousClass = new Runnable() {
            @Override
            public void run() {
                System.out.println("익명 클래스: " + message);
            }
        };

        // 람다식 사용
        Runnable lambda = () -> {
            System.out.println("람다식: " + message);
        };

        // 외부 변수 변경 시도
        // message = "Hello, Java!";  // ❌ 컴파일 오류 발생

        anonymousClass.run();
        lambda.run();
    }
}

💥 왜 message = "Hello, Java!";에서 컴파일 오류가 발생할까?

람다식과 익명 클래스는 외부 지역 변수를 참조할 때, 해당 변수가 변경되지 않을 것이라는 보장이 필요하다.

람다가 생성될 때 message의 값인 "Hello, World!"를 복사해서 가지고 있기 때문에,

나중에 이 변수를 변경하면 람다 내부의 값과 불일치가 발생할 수 있다.

이를 방지하기 위해 effectively final 규칙을 적용하는 것이다.

 

🔍 람다가 생성될 당시 값 복사의 의미

람다식은 참조하는 변수를 실시간으로 참조하는 것이 아니라, 람다가 생성될 때의 값을 캡처한다.

바로 위의 예제에서 message = "Hello, Java!"로 값을 변경하려 하면 컴파일 오류가 발생하는 이유는

람다가 message의 현재 값("Hello, World!")을 캡처했기 때문에, 이후 변경되는 값을 허용하지 않는다는 것을 보여준다.

 

🏃 반면, 필드나 인스턴스 변수는 변경 가능!

람다식에서는 지역 변수가 아닌 클래스의 필드 변수는 자유롭게 변경할 수 있다.

public class FieldExample {
    private int number = 10;

    public void execute() {
        Runnable lambda = () -> System.out.println("람다식: " + number);
        number = 20;  // ✅ 필드 변수는 변경 가능
        lambda.run();  // 출력: 람다식: 20
    }

    public static void main(String[] args) {
        new FieldExample().execute();
    }
}

클래스의 필드 변수는 힙 메모리에 저장되며, 객체와 함께 존재하기 때문에 실시간 참조가 가능하다.

반면, 지역 변수는 스택 메모리에 존재하며 메서드가 종료되면 소멸되기 때문에, 값 복사가 필요하다.

 

 

✅ 정리 요약

왜 message는 변경 불가?

message는 람다식 안에서 캡처되는 지역 변수.

자바는 람다에서 사용하는 지역 변수는 반드시 final 또는 final처럼 불변이어야만 허용한다.

람다 내부에서는 message 값을 복사해서 따로 저장하기 때문. 그런데 외부에서 바꿔버리면 어떤 값을 참조해야 할지 모호해지기 때문이다.

 

왜 number는 변경 가능?

이 number는 클래스의 필드(멤버 변수)로, 힙 메모리 상에 존재하고

람다식은 해당 객체를 참조하고 있으므로(필드는 객체의 일부이기 때문에 참조값으로 접근),

값이 나중에 바뀌더라도 실시간으로 최신 값이 반영된다. ㅇㅇㅇ


📜 3) 클래스 이름을 통한 인스턴스 메서드 참조 (ClassName::instanceMethod)

예제: String::toUpperCase 참조

import java.util.*;
import java.util.stream.*;

public class ArbitraryObjectMethodReferenceExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("java", "lambda", "stream");
        words.stream()
             .map(String::toUpperCase)  // 각 문자열을 대문자로 변환
             .forEach(System.out::println);
    }
}

String::toUpperCaseString 타입의 모든 객체에 적용 가능한 메서드이다.

람다식 대체: s -> s.toUpperCase()

 

 

 

참조된 메서드는 첫 번째 매개변수를 메서드 참조의 인스턴스 객체로 사용한다.

람다식과 동일한 의미:

(obj, args...) -> obj.instanceMethod(args...)

 

 

추가 예제:

import java.util.*;

class Employee {
    String name;

    Employee(String name, int age, double salary) { this.name = name; }
    
    public int compareByName(Employee other) {
        return this.name.compareTo(other.name);
    }
}

public class AdvancedExample {
    public static void main(String[] args) {
        List<Employee> employees = Arrays.asList(
            new Employee("Alice"),
            new Employee("Bob"),
            new Employee("Charlie")
        );

        // ✅ 이름 기준 정렬
        employees.sort(Employee::compareByName);
        System.out.println("이름 기준 정렬: " + employees);
    }
}

Employee::compareByName: (e1, e2) -> e1.compareByName(e2)와 동일

자바에서 List의 sort() 메서드는 다음과 같이 정의되어 있다:

default void sort(Comparator<? super E> c)

그리고 Comparator<Employee>는 다음과 같은 메서드를 구현해야 한다:

int compare(Employee e1, Employee e2)

 

정렬 과정 중 다음과 같이 비교가 이루어진다.

 

compareByName이 호출될 때 예시:

  • e1 = Employee("Alice"), e2 = Employee("Bob")
  • e1.compareByName(e2) → "Alice".compareTo("Bob")

그 다음:

  • e1 = Employee("Bob"), e2 = Employee("Charlie")
  • e1.compareByName(e2) → "Bob".compareTo("Charlie")

 


📜 4) 생성자 참조 (ClassName::new)

예제: ArrayList::new 참조

import java.util.*;
import java.util.function.Supplier;

public class ConstructorReferenceExample {
    public static void main(String[] args) {
        Supplier<List<String>> listSupplier = ArrayList::new;
        List<String> list = listSupplier.get();
        list.add("Hello");
        list.add("World");
        list.forEach(System.out::println);
    }
}

ArrayList::new는 ArrayList의 생성자를 참조한다.

람다식 대체: Supplier<List<String>> listSupplier = () -> new ArrayList<>()

와 같다.

 

 

추가 예제:

import java.util.*;
import java.util.function.Function;

public class ConstructorReferenceExample2 {
    public static void main(String[] args) {
    	Function<char[], String> f = String::new;
        
        char[] src = {'R', 'o', 'b', 'o', 't'};
        String str = f.apply(src);
        System.out.println(src);
    }
}

람다식 대체: Function<char[], String> f = ar -> { return new String(ar); };

                                                               = ar -> new String(ar);

와 같다.