[Java vs Python] 람다 한 장 정리

NANO·5일 전

알고리즘 Cheat Sheet 시리즈는 코딩 테스트를 풀다가 "이거 자바에선 뭐였지?", "파이썬은 어떻게 했더라?" 싶을 때 바로 펼쳐 보려고 만든 개인 참고용 정리입니다.
Java와 Python을 나란히 놓고, 문법 차이 때문에 실수하기 쉬운 부분만 짧게 정리합니다.

이번 주제는 람다입니다!
람다는 쉽게 말하면 이름 없는 짧은 함수입니다. 코테에서는 주로 정렬 기준, 변환(map)/거르기(filter), 입력 파싱에 씁니다.


1. 기본 문법

형태자바파이썬
인자 1개x -> x * 2lambda x: x * 2
인자 2개(a, b) -> a + blambda a, b: a + b
인자 없음() -> 42lambda: 42
여러 줄(a, b) -> { int s = a + b; return s * 2; }불가능 (식 하나만)
조건 분기x -> x % 2 == 0 ? "짝" : "홀"lambda x: "짝" if x % 2 == 0 else "홀"
// 자바 람다는 반드시 "함수형 인터페이스" 타입에 담아야 한다
Function<Integer, Integer> doubleIt = x -> x * 2;
BinaryOperator<Integer> add = (a, b) -> a + b;

doubleIt.apply(3);   // 6
add.apply(2, 3);     // 5
double_it = lambda x: x * 2
add = lambda a, b: a + b

double_it(3)   # 6
add(2, 3)      # 5

파이썬 람다는 식(expression) 하나만 쓸 수 있다. if문, for문, 대입이 필요하면 def로 함수를 만든다.
자바 람다는 { } 블록을 쓰면 여러 줄도 가능하지만, 이때는 return을 직접 써야 한다.

자바 함수형 인터페이스

자바 람다는 혼자 존재할 수 없고, 어떤 인터페이스의 구현으로 쓰인다. 자주 보는 것만 알아두면 된다.

인터페이스모양호출주로 쓰는 곳
Comparator<T>(a, b) -> intcompare(a, b)정렬
Function<T, R>x -> 결과apply(x)변환 (map)
Predicate<T>x -> booleantest(x)조건 (filter, removeIf)
Consumer<T>x -> { } (반환 없음)accept(x)forEach
Supplier<T>() -> 값get()computeIfAbsent의 새 객체 생성 등
BinaryOperator<T>(a, b) -> 같은 타입apply(a, b)reduce, merge

2. 메서드 참조 vs 함수 그대로 넘기기

람다가 이미 있는 함수를 호출만 할 때는 더 짧게 쓸 수 있다.

하고 싶은 일자바 람다자바 메서드 참조파이썬
문자열 → 정수s -> Integer.parseInt(s)Integer::parseIntint
길이s -> s.length()String::lengthlen
두 수 더하기(a, b) -> a + bInteger::sumoperator.add
출력x -> System.out.println(x)System.out::printlnprint
새 리스트 생성() -> new ArrayList<>()ArrayList::newlist
map.merge(key, 1, Integer::sum);
words.sort(Comparator.comparing(String::length));
words.sort(key=len)                 # 함수 이름을 그대로 넘긴다
nums = list(map(int, ["1", "2"]))   # int 함수를 그대로 넘긴다

파이썬은 함수 자체가 값이라서 key=len처럼 괄호 없이 이름만 넘기면 된다. key=len()이라고 쓰면 에러 발생!


3. 정렬 기준 (복습)

기준자바파이썬
오름차순(a, b) -> Integer.compare(a, b)기본값
내림차순(a, b) -> Integer.compare(b, a)reverse=True
길이순Comparator.comparing(String::length)key=len
길이 내림차순Comparator.comparing(String::length).reversed()key=len, reverse=True
1순위 → 2순위.thenComparing(...)key=lambda x: (x[0], x[1])
  • 자바는 "두 값을 받아 비교", 파이썬은 "값 하나를 받아 비교할 기준을 반환" 하는 방식이다.
  • 자바 Comparator.comparing(...)이 파이썬 key=와 같은 방식이라, 익숙해지면 둘을 똑같이 생각할 수 있다.
// 파이썬 key=lambda x: (x[0], -x[1]) 과 같은 정렬
Arrays.sort(arr, Comparator.comparingInt((int[] x) -> x[0])
                           .thenComparing((int[] x) -> x[1], Comparator.reverseOrder()));

자세한 정렬은 #2 형변환/비교/정렬 편 참고


4. map · filter · reduce

기능자바 (스트림)파이썬 (함수)파이썬 (컴프리헨션)
모두 변환.map(x -> x * 2)map(lambda x: x * 2, arr)[x * 2 for x in arr]
조건 거르기.filter(x -> x > 0)filter(lambda x: x > 0, arr)[x for x in arr if x > 0]
하나로 합치기.reduce(0, Integer::sum)reduce(lambda a, b: a + b, arr, 0)sum(arr)
int[] arr = {1, -2, 3, -4, 5};

int[] result = Arrays.stream(arr)
                     .filter(x -> x > 0)     // 1, 3, 5
                     .map(x -> x * 2)        // 2, 6, 10
                     .toArray();

List<String> words = List.of("apple", "kiwi", "banana");
List<Integer> lengths = words.stream()
                             .map(String::length)
                             .collect(Collectors.toList());   // [5, 4, 6]
arr = [1, -2, 3, -4, 5]

result = [x * 2 for x in arr if x > 0]              # [2, 6, 10]  ← 추천
result = list(map(lambda x: x * 2,
                  filter(lambda x: x > 0, arr)))    # 같은 결과

from functools import reduce
total = reduce(lambda a, b: a + b, arr, 0)          # 3

파이썬에서는 map + lambda 조합보다 리스트 컴프리헨션이 더 읽기 쉽고 흔하다.
단, map(int, ...)처럼 이미 있는 함수를 넘길 때는 map이 더 짧다.


5. 코테에서 자주 쓰는 한 줄

상황자바파이썬
입력 한 줄 → 정수 배열Arrays.stream(br.readLine().split(" ")).mapToInt(Integer::parseInt).toArray()list(map(int, input().split()))
가장 긴 문자열Collections.max(words, Comparator.comparing(String::length))max(words, key=len)
값이 가장 큰 키Collections.max(map.entrySet(), Map.Entry.comparingByValue()).getKey()max(d, key=d.get)
조건에 맞는 개수list.stream().filter(x -> x > 0).count()sum(1 for x in lst if x > 0)
조건에 맞는 원소 삭제list.removeIf(x -> x < 0)lst = [x for x in lst if x >= 0]
모두 변환 (원본 수정)list.replaceAll(x -> x * 2)lst = [x * 2 for x in lst]
맵 순회map.forEach((k, v) -> ...)for k, v in d.items():
하나라도 / 모두.anyMatch(x -> x > 0) / .allMatch(...)any(x > 0 for x in lst) / all(...)

6. 체크포인트

[자바] 람다 안에서 바깥 지역변수를 바꿀 수 없다

람다가 쓰는 바깥 지역변수는 사실상 final(한 번도 바뀌지 않는 값)이어야 한다.

int count = 0;
list.forEach(x -> count++);        // ❌ 컴파일 에러

int[] count = {0};
list.forEach(x -> count[0]++);     // ⭕ 배열 원소는 바꿀 수 있다 (꼼수)

int cnt = 0;
for (int x : list) cnt++;          // ⭕ 그냥 for문이 제일 깔끔

[파이썬] 반복문 안에서 만든 람다는 마지막 값을 본다

funcs = [lambda: i for i in range(3)]
[f() for f in funcs]               # [2, 2, 2]  ← 0, 1, 2가 아님!

funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]               # [0, 1, 2]  ← 기본값으로 현재 i를 고정

람다는 i의 값이 아니라 변수 자체를 기억하기 때문에, 호출 시점의 i(마지막 값 2)를 읽는다.

그 밖에

  • 파이썬 map / filter는 결과를 바로 만들지 않는다. 이터레이터라서 list()로 감싸야 리스트가 되고, 한 번 순회하면 다시 쓸 수 없다.
  • 자바 스트림도 한 번만 쓸 수 있다. 다시 쓰면 IllegalStateException이 난다.
  • 자바 스트림은 for문보다 느리다. 반복 횟수가 많은 핵심 로직에서는 for문을 쓰고, 스트림은 입력 파싱처럼 한 번만 실행되는 곳에 쓰자.
  • 자바 int[] 스트림은 IntStream이다. mapToInt, boxed()로 Stream<Integer>와 오가야 할 때가 있다 #4 참고
  • 자바 람다 + .reversed()는 타입 추론이 안 될 때가 있다. Comparator.comparing((String s) -> s.length()).reversed()처럼 매개변수 타입을 적어 준다.

💡 이것만 기억하자

  1. 문법:
    자바: (a, b) -> 식
    파이썬: lambda a, b: 식
    , 파이썬은 식 하나만
  2. 이미 있는 함수는 그대로 넘기자.
    자바: Integer::parseInt
    파이썬: int, len
  3. 정렬: 자바는 두 값을 비교
    파이썬: 기준 값을 반환(key=)
    자바: Comparator.comparing이 파이썬 key=와 같은 방식
  4. 변환/거르기:
    자바: 스트림 map / filter
    파이썬: 리스트 컴프리헨션
  5. 자바 람다 안에서 바깥 변수 수정 불가! 카운트는 for문으로
  6. 파이썬 반복문 안 람다는 lambda i=i:로 값을 고정
profile
즐거운 토마토

0개의 댓글