[2025백엔드] 헤드퍼스트자바 독서 #8 - 12장. 람다와 스트림

2025. 8. 13. 15:28·언어공부/Java | Kotlin

오늘의 마치

와! 몰랐던거 두 개! 람다와 스트림!

가장 배우고 싶었던 것이다. 파이썬 때도 람다는 몰라서 근데 가만 생각해보니 나 파이썬으로 왜이렇게 뭔가를 안했지

 

암튼


람다1 - forEach

기존 for문은 for 반복문의 조건을 정하고, 얼마나 돌지 정하고 (이것도 C언어 기초처럼 int i = 0, i++ 이런 방식이 아니라 향상된 for문을 쓰면 편하긴 한데 그래도 귀찮아) 등을 선언해야했다. 하지만

 

List<String> names = List.of("마치", "만치");
names.forEach(name -> System.out.println(name)); //마치\n만치

를 사용하면 한 줄로도 리스트를 순회하며 원소를 출력한다.

forEach는 Iterable 인터페이스에 선언되어 있으며, List가 Iterable을 구현하기 때문에 List도 사용가능하다. 

 

정확히는 이 게시글을 참고하면 좋을 듯 하다. -> https://blog.naver.com/writer0713/220877874725

 

[java] Collection과 Iterator (Iterable)

List와 Set은 공통적으로 Iterator을 사용할 수 있다. 그럼 두 인터페이스가 공통적으로 가지고 있는 이...

blog.naver.com

컬렉션이 Iterable의 구현체이기 때문에 컬렉션의 하위요소인 List, Set은 Iterable을 사용가능하다.


스트림

스트림 API는 컬렉션에 대해서 수행할 연산의 집합이다. 대표적으로

 

Stream<T> distinct() (서로 다른 원소들로 구성된 스트림 반환)

Stream<T> filter(Predicate<? super T> predicate) / 주어진 서술에 맞는 원소 스트림 리턴

Stream<T> limit(long maxSize) / 길이가 maxSize보다 작도록 잘라낸 스트림 리턴

<R> Stream<R> map(Function<? supaer t,? extends R> mapper>) / 함수를 스트림의 원소에 적용한 결과의 스트림 리턴

Stream<T> skip(long n) / 앞쪽 원소 n개 제거

Stream<T> sorted() / 자연스러운 정렬

 

등이 있다.

스프림도 람다와 함께 자바8에서 도입되었다.


스트림 스타트

스트림 객체를 만들어야한다.

List<String> strings = List.of("I", "am", "your", "father");
Stream<String> stream = strings.stream();

을 통하면 스트림을 불러올 수 있다. 참고로 stream() 메서드는 컬렉션에서 지원하기 때문에 컬렉션의 하위클래스들도 사용할 수 있다.

 

Stream<String> limit = stream.limit(4)
System.out.println(limit);

다만 이러면 원하는 값이 출력되지 않는다. java.util.stream.SliceOps$1@6d06d69c 등의 이상한 값이 출력되는데, 스트림에는 컬렉션에 들어있는 원소들이 들어있는게 아니라 스트림 객체가 컬렉션에 대해 수행할 명령어의 집합이다.

이처럼 스트림에서 다른 스트림을 리턴하는 메서드를 중간연산(Intermediate operation)이라고 부르며, 실제 연산을 수행하지는 않지만 어떻게 연산해야할 지 정의하는 역할을 한다.

 

최종연산

이렇게 중간연산은 컬렉션에 대하여 어떤 연산을 해야할 지 정의하는 역할을 하지만, 실제로 적용된 연산을 리턴하기 위해서는 최종 연산이라는 것이 필요하다.

 

boolean anyMatch(Predicate<? super T> predicate) / 주어진 서술에 맞는 원소가 하나라도 있을 경우 참

long count() / 원소의 개수

<R, A> R collect (Collector<? super T,A,R> collector) / Collector를 이용하여 원소에 대한 가변 축소 연산 수행

Optional<T> findFirst() / 첫 번째 원소 반환. 스트림이 비어있으면 빈 Optional 리턴

 

스트림으로 흔하게 하는 일은 연산을 수행하고 다른 컬렉션에 이 결과를 집어넣어주는 역할을 한다.

List<String> result = limit.collect(Collectors.toList());

같은 방식으로 collect 메서드를 사용하여 컬렉션에 연산된 인자를 넣어서 반환할 수 있다.


스트림 파이프라인

위처럼 컬렉션에서 스트림을 만들고, 스트림에 대해 연산을 수행한 후, 다시 최종연산을 통해 결과를 가져오는 일련의 과정을 스트림 파이프라인이라고 한다.

 

List<String> result = strings.stream()
		.limit(4)
		.collect(Collectors.toList());

이런 스트림연산은 위처럼 스트림 만드는 것 따로, 연산따로, 출력따로 할 필요없이 코드 한출로만으로도 한 번에 처리가 가능하다. ('사슬처럼 줄줄이 엮을 수 있다') 중간연산도 limit() 하나 뿐 아니라 다른 여러 개의 중간연산을 스트림 파이프에 넣을 수 있다. 가령 sorted()나 filter()라던지, 메서드 인자로 람다를 전해줄 수도 있다.

 

List<String> result = strings.stream()
		.limit(4)
        	.skip(2)
        	.sorted((s1, s2) -> s1.compareToIgnoreCase(s2))
		.collect(Collectors.toList());

이렇게 일련의 스트림 연산과정, 스트림 파이프라인이 완성되었다.

스트림의 중간연산에는 느긋한 계산법 (Lazy evaluation)이 적용된다. 위에도 나왔지만 어떤 연산을 진행할지 미리 지정해두기만 하고 최종연산에서만 이 지정된 연산을 확인하고 모든 중간연산을 한꺼번에 실행시킨다. 최종연산은 조급한 계산법 (eager evaluation)이 적용된다.


스트림은 어떻게 쓸까

스트림에 대해서 몇 가지 알아야할 사항만 알면 능숙하게 사용할 수 있겠지?

 

1. 스트림의 최소조건은 스트림을 만드는 것과 최종연산으로 반환되는 값이 있어야한다.

2. 최종연산이 호출된 스트림은 다시 재사용할 수 없다.

3. 스트림이 작동하는 동안 스트림에 들어가는 컬렉션은 바꿀 수 없다.

4. 스트림은 컬렉션 자체를 바꾸지 않는다.

5. 중간연산의 정의 순서에 따라 최종연산되는 값이 바뀐다

 


람다

람다는 SAM의 단일 추상 메서드를 호출하여 실행시키는 객체이며, 람다 표현식은 함수형 인터페이스의 구현체이다.

따라서 람다 표현식의 타입은 함수형 인터페이스다.

 

(s1, s2) -> s1.compareToIgnoreCase(s2)

이런식으로 왼쪽에 필요한 변수들을 준비해두고 오른쪽의 메서드에 이 변수들을 넘기는 형태로 람다식을 작성할 수 있다.

 

이를 return문이 포함되게 풀어보면

(String s1, String 2) ->
{
	reutrn s1.compareToIgnoreCase(s2);
}

이렇게 풀어볼 수 있다.

람다 표현식에서 매개변수의 타입은 함수형 인터페이스가 이미 결정하지만 모양이 같은 함수형 인터페이스가 여러 개일 경우에는 명시적으로 표기해야한다.

또한 람다 표현식이 두 줄 이상이라면 반드시 중괄호로 묶어서 표현해야하고, 이 때는 뒤에 세미콜론이 붙어야한다. return문도 필요하다.

 

람다의 매개변수, 리턴타입, 기능들은 람다가 구현하는 함수형 인터페이스에 의해 결정된다.


람다규칙

(str1, str2) -> {
	int l1 = str1.length();
    int l2 = str2.length();
    return l2 - l1;
}

위는 Comparator의 구현체로, 위처럼 두 줄 이상의 코드가 올 수도 있으며, 이럴 경우에는 중괄호로 감싸고 return을 해야한다. 11장에서 나왔던 sort() 메서드에 이를 넣어줄 수 있다.

 

다만 함수형 인터페이스의 메서드가 void로 선언된 경우에는 return문이 없어도 된다.

(str1, str2) -> str2.length() - str1.length()

이렇게 한 줄짜리로 간단하게 만들 수도 있다. 기능은 위와 같다.

 

매개변수를 전달할 때

매개변수가 없으면 () <- 빈괄호를 넣고, 2개 이상일 때도 괄호 안에 매개변수를 넣어서 전달해야한다. 1개일 경우에는 괄호가 없어도 된다.

 

public interface Consumer<T> {
	void accept(T t);
}

...

Consumer<String> c = s -> System.out.println(s);
...

와 같이 람다표현식을 변수에 저장한다면, 람다 표현식이 Consumer 인터페이스의 구현체가 되는 것이다.

 

람다 표현식에서 전해주어야할 인자의 개수와 타입은는 SAM이 받는 매개변수의 개수 및 타입과 같으며 (타입은 호환가능하면 된다), 메서드가 return 해야할 경우 return 타입도 람다 표현식의 리턴 타입과 맞춰야한다. 저 위에를 예시로 들면 리턴타입이 없는거지 출력을 해버렸으니

람다 표현식에서 화살표 오른쪽에 있는 식이 계산과정과 리턴타입을 결정한다.

 

Consumer 인터페이스의 메서드가 매개변수를 1개 받으므로 s 하나만 전해준다던가 하는식으로

매개변수 타입은 선언하지 않아도 되지만 가독성을 위해서 적어두면 좋다. 또한 컴파일러가 표현식이 어떤 함수형인터페이스를 구현하는지 알 수 없는 경우에라면 적어줘야한다.

 

다시 한 번 정리하자면, 람다 표현식은 함수형 인터페이스의 익명 구현체이며, 어떤 함수형 인터페이스를 구현할지는 변수에 저장될 경우 변수에 저장할 타입이 결정하거나 컴파일러가 추측할 수 있다.

람다 표현식의 매개변수(화살표 좌측)는 함수형 인터페이스의 메서드의 매개변수와 개수가 같고 타입이 호환가능해야하며, 메서드의 리턴 타입과 람다 표현식의 리턴타입이 호환되야한다. 람다 표현식의 리턴타입은 화살표 우측에서 실행된 코드가 진행된 후 반환되는 타입이다.


함수형 인터페이스 찾기

@FunctionalInterface 어노테이션을 사용하면 람다 표현식으로 구현될 수 있는 SAM이 있다는 것을 알릴 수 있다. 하지만 이 어노테이션이 없는 경우도 있다. 다만 단순히 '메서드가 한 개뿐인 인터페이스만 찾기'로만 되는게 아니다.

 

자바 8부터는 인터페이스에 추상클래스와 기본메서드, 정적메서드까지 들어올 수 있다. 문제는 람다 표현식은 추상클래스만의 구현체로 사용해야만 한다.

 

자바 API를 보면 '추상메서드'가 하나뿐인 인터페이스가 있는게 그게 바로 함수형 인터페이스이다. 추상클래스가 하나인가만 구분하면 되고 정적메서드나 기본메서드의 개수는 상관하지 않는다.

※Object 클래스에서 상속받은 메서드들은 무시해도 된다.


스트림 필터

리스트들이 쭈르륵 있을 때 특정 조건을 만족하는 경우만 리스트에서 뽑아내고 싶은 경우가 있다. 이럴 때 사용해야할 것이 리스트를 스트림에 넣고 스트림의 메서드 .filter()를 사용하는 것이다.

 

필터 메서드는 Predicate 를 받아들이는데, 이는

@FunctionalInterface
public interface Predicate<T> {
	boolean test(T t);
}

이렇게 생겼다. 나보다 잘생김

함수형 인터페이스이기 때문에 람다 표현식을 사용할 수 있다.

 

Predicate predicate = (매개변수) -> (식)

형식으로 람다를 Predicate의 람다를 구성하여 필터메서드에 전달할 수 있다.

 

매개변수 부분은 test가 받는 매개변수가 될 것이고 (그래서 개수는 1개), 식 부분은 test 메서드의 구현체가 될부분으로, 리턴 타입이 boolean이 되도록 설정해야한다.

 

대충대충 예시를 들어보자면 (많은코드가 생략되었지만)

List<Member> students = members.stream()
			.filter(member -> member.getjob().equals("student"))
                        .collect(Collectors.toList());

이런식으로 구성된다. Member 객체들 중에서 직업이 student인 객체만 뽑아내서 리스트를 만들어 반환한다.

 

근데 직업이 student도 있지만 highschool student도 있고 middleschool student 등등 많이 있다면 String에 있는 .contains 메서드를 사용하면 된다.

List<Member> students = members.stream()
			.filter(member -> member.getjob().contains("student"))
                        .collect(Collectors.toList());

이렇게. 그러면 직업에 student가 들어가있는 모든 Member 객체들을 뽑아서 리스트에 넣어 반환한다.


스트림 맵

스트림의 요소들을 한 타입에서 다른 타입으로 바꿔준다. 파이썬에도 비슷한게 있던 것 같은데 파이썬은 어디서나 쓸 수 있었고 자바에서는 스트림의 메서드로만 사용할 수 있었다.

 

Map 메서드는 인자로 Function을 받아들이며, 이 역시 함수형 인터페이스이다.

@FunctionalInterface
public interface Function<T, R> {
	R apply(T t);
}

 

유저의 직업만을 반환하고 싶다면

List<String> jobs = members.stream()
		.map(member -> member.getJob())
         	.collect(toList());

이렇게. 람다 표현식이 Function 클래스의 apply의 메서드의 구현체가되며, T를 인자로 받으니 member를 매개변수로 전해주고, 리턴 타입 R은 String으로 정해지게 된다.

 

이렇게하면 members에 대한 스트림을 만든 다음에 map 함수를 통해 String에 대한 스트림으로 변환한 다음에 이를 리스트에 넣어주는 함수다.

 

map 연산은 '한 타입에서 다른 타입으로 매핑하는 방법을 알려준다'라고 했으므로 members에 대한 스트림을 String에 대한 스트림으로 어떻게 바꿀지 지정해주는 것이라고 볼 수 있겠다.

 

List<String> jobs = members.stream()
		.map(member -> member.getJob())
        	.distinct()
         	.collect(toList());

다만 job은 굳이 중복될 필요가 없으므로 distinct 메서드를 통해 중복을 제거하도록 한다.

아까 말한 스트림메서드의 순서가 중요한 이유가 distinct가 먼저 와버리면 직업에 대한 리스트만 뽑으려는게 무용지물이다.

이런식으로 스트림 메서드를 쌓아가며 연산을 지속할 수 있다.

 

굳이 람다를 써야할까?

스트림의 map 메서드를 사용할 때 람다를 쓰지 않으면 Function 클래스를 직접 만들어서 매개변수로 넘겨야하고, 굳이 그럴 필요가 없다면 람다를 사용해도 된다. 그런데 람다 표현식도 필요하지 않을 때가 있다.

 

Function<Member, String> getJob = member -> member.getJob();

이런식으로 Function 클래스를 만들었고, 람다 표현식이 apply의 구현체로 들어가게 된다.

Function<Member, String> getJob = Member::getJob;

람다 표현식을 일일이 적는 대신에 메서드 레퍼런스를 사용하면 된다. 컴파일러에게 연산을 처리해 주는 메서드를 알려준다.

Function 호출부분까지는 같다. Member 객체에 의미 정의된 getJob() 메서드의 레퍼런스만 호출하면 된다. '.'을 써서 호출하는게 아니라 '::'를 써서 컴파일러에게 메서드이름만 알려준다.

 

List<String> jobs = members.stream()
		.map(Member::getJob)
         	.collect(toList());

이렇게하면 스트림에 들어오는 Member 객체들마다 getJob 메서드를 적용시켜서 리스트로 바꾼다.

List<Member> memberOrderAge = members.stream()
			.sorted((m1, m2) -> m1.getAge() - m2.getAge())
           		.collect(toList());

이렇게 람다 표현식으로 Comparator를 정의한 것 외에 메서드 레퍼런스를 써보자면

List<Member> memberOrderAge = members.stream()
			.sorted(Comparator.comparingInt(Member::getAge))
           		.collect(toList());

이렇게 써보면 된다.

comparingInt는 정적 보조메서드이다.

 

클래스와 메서드들을 하나하나 다 외울 수는 없어서 API 문서를 자주자주 읽어봐야할 것 같다.

다만 메서드 레퍼런스는 람다 표현식과 메서드 레퍼런스 중에 하나를 선택하면 되는거고 가독성이 좀 떨어지거나 별로면 그냥 람다 표현식을 써도 ㅇㅋ


최종연산하기 

컬렉터 시리즈

...
.collect(Collectors.toList());

방식을 지금까지 사용해왔다. 스트림을 리스트로 변경하는 메서드인데, toList를 toSet으로 바꾸면 Set 타입으로 스트림이 변경되게 된다.

 

이외에도

- toList()와 toUnmodifiableList() 를 통해 리스트 - 불변리스트로 스트림의 최종연산이 가능하다.

- toSet()과 toUnmodifiableSet() 으로도 Set으로도 가능하다.

- toMap과 toUnmodifiableMap을 통해 Map - 불변Map으로 변형할 수 있다. 다만 어떤 것을 키로, 어떤 것을 값으로 저장할 지에 대한 함수를 전달해주어야한다.

- joining()은 스트림으로 뽑은 결과를 String으로 반환할 수 있다. 각 원소를 구분할 문자 (',' 같은)를 지정하여 같이 넘겨줄 수도 있다. CSV 만들 때 유용하다.

 

anyMatch 시리즈

anyMatch를 사용하면 존재여부를 불리언값으로 리턴한다.

boolean anyMatch(Predicate p);
boolean allMatch(Predicate p);
boolean noneMatch(Predicate p);

Predicate는 위에서 주었고, 참 / 거짓으로 구분할 수 있는 기준을 전해주기만하면 된다.

 

boolean result = members.stream()
			.anyMatch(member -> member.getJob().equals("student"));

anyMatch의 대표적인 예시. 있으면 true를 반환한다.

 

찾기 시리즈

Optional<T> findAny();
Optional<T> findFirst();
Optional<T> max (Comparator c);
Optional<T> min (Comparator c);
Optional<T> reduce(BinaryOperator a);

이렇게 있다. Comparator는 이전에 알아봤던 그거 맞다.

 

Optional<Member> result = members.stream()
		.filter(member -> member.getAge() == 20)
           	.findFirst();

저렇게 filter로 걸러놓고 가장 앞에 있는 하나만 뽑으면 된다.

 

개수 세기 시리즈 (사실 시리즈 아님)

long count();

이러면 원소의 개수를 구할 때 쓸 수 있다.

 

물론 이런거 말고도 많은 최종연산들이 있지만, 당연히 다는 기억하지 못한다. 그러면 암기왕해서 세상에 이런 일이에 나가야지

자바 API 문서를 활용하면 메서드들과 타입 등의 조건들에 대해서도 알 수 있다. 나중에 한 번 찾아봐야겠는걸


null을 막자 null을 막자 널널널 - Optional

Optional은 값을 래핑하는 래퍼다. .ispresenct() 메서드를 통해 비었는지 안비었는지 알아볼 수 있다. true가 내부에 원소가 하나라도 있고, false가 없다.

래핑한 것에서 원래 객체를 가져오려면 .get 메서드를 사용하면 된다.

 

후아

람다가 궁금해서 좀 배우려고 했는데 시간이 다소 걸렸다. 그래도 배움 왕뿌뿌듯

우선은 람다를 통해서 어떤 것을 구현해야하는지, 그리고 어떤 것을 인자로 주고 어떤 타입을 정해야하는지 등을 좀 알아놓는게 중요한 것 같다. 자바 API 문서.. 종종 읽어보면 좋을 것 같다. 그럴시간에 책을 읽으면 더 좋고

'언어공부 > Java | Kotlin' 카테고리의 다른 글

[2025 백엔드] 헤드퍼스트자바 독서 #10 - 16장. 객체 저장  (5) 2025.08.17
[2025 백엔드] 헤드퍼스트자바 독서 #9 - 13장. 위험한 행동  (4) 2025.08.13
[2025백엔드] 헤드퍼스트자바 독서 #7 - 11장. 자료구조  (5) 2025.08.13
[2025백엔드] 헤드퍼스트자바 독서 #6 - 10장. 숫자는 정말 중요합니다  (5) 2025.08.03
[2025백엔드] 헤드퍼스트자바 독서 #5 - 9장. 객체의 삶과 죽음  (3) 2025.07.27
'언어공부/Java | Kotlin' 카테고리의 다른 글
  • [2025 백엔드] 헤드퍼스트자바 독서 #10 - 16장. 객체 저장
  • [2025 백엔드] 헤드퍼스트자바 독서 #9 - 13장. 위험한 행동
  • [2025백엔드] 헤드퍼스트자바 독서 #7 - 11장. 자료구조
  • [2025백엔드] 헤드퍼스트자바 독서 #6 - 10장. 숫자는 정말 중요합니다
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211)
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59)
        • JVM (12)
        • 백엔드 (20)
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[2025백엔드] 헤드퍼스트자바 독서 #8 - 12장. 람다와 스트림
상단으로

티스토리툴바