람다활용
람다 활용 - 필터
V1 - 람다를 사용하지 않는 방식
import java.util.ArrayList;
import java.util.List;
public class FilterMainV1 {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 10);
List<Integer> evens = filterEvenNumber(numbers);
List<Integer> odds = filterOddNumber(numbers);
}
//짝수만 거르기
private static List<Integer> filterEvenNumber(List<Integer> numbers) {
List<Integer> filtered = new ArrayList<>();
for (int num : numbers) {
if (num % 2 == 0) {
filtered.add(num);
}
}
return filtered;
}
//홀수만 거르기
private static List<Integer> filterOddNumber(List<Integer> numbers) {
List<Integer> filtered = new ArrayList<>();
for (int num : numbers) {
if (num % 2 == 1) {
filtered.add(num);
}
}
return filtered;
}
}V2 - 람다를 사용해서 중복을 제거한 방식
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class FilterMainV2 {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 10);
List<Integer> even = filter(numbers, num -> num % 2 == 0);
List<Integer> odd = filter(numbers, num -> num % 2 == 1);
}
private static List<Integer> filter(List<Integer> numbers, Predicate<Integer> predicate) {
List<Integer> filtered = new ArrayList<>();
for (int num : numbers) {
if (predicate.test(num)) {
filtered.add(num);
}
}
return filtered;
}
}V3 - 별도의 유틸리티 클래스 작성
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class IntegerFilter {
public static List<Integer> filter(List<Integer> numbers, Predicate<Integer> predicate) {
List<Integer> filtered = new ArrayList<>();
for (int num : numbers) {
if (predicate.test(num)) {
filtered.add(num);
}
}
return filtered;
}
}import java.util.List;
public class FilterMainV3 {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 10);
List<Integer> even = IntegerFilter.filter(numbers, num -> num % 2 == 0);
List<Integer> odd = IntegerFilter.filter(numbers, num -> num % 2 == 1);
}
}V4 - 제네릭 사용
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class GenericFilter {
public static <T> List<T> filter(List<T> values, Predicate<T> predicate) {
List<T> filtered = new ArrayList<>();
for (T val : values) {
if (predicate.test(val)) {
filtered.add(val);
}
}
return filtered;
}
}import java.util.List;
public class FilterMainV4 {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 10);
List<Integer> even = GenericFilter.filter(numbers, num -> num % 2 == 0);
List<Integer> odd = GenericFilter.filter(numbers, num -> num % 2 == 1);
List<String> strings = List.of("A", "BB", "CCC");
List<String> result1 = GenericFilter.filter(strings, s -> s.length() >= 2);
List<String> result2 = GenericFilter.filter(strings, s -> s.length() % 2 == 1);
}
}람다 활용 - 매핑
V1 - 람다를 사용하지 않는 방식
import java.util.ArrayList;
import java.util.List;
public class MapMainV1 {
public static void main(String[] args) {
List<String> list = List.of("1", "12", "123", "1234");
List<Integer> numbers = mapStringToInteger(list);
List<Integer> lengths = mapStringToLength(list);
}
//문자열의 길이
private static List<Integer> mapStringToLength(List<String> list) {
List<Integer> result = new ArrayList<>();
for (String s : list) {
result.add(s.length());
}
return result;
}
//문자열을 숫자로 반환
private static List<Integer> mapStringToInteger(List<String> list) {
List<Integer> result = new ArrayList<>();
for (String s : list) {
result.add(Integer.valueOf(s));
}
return result;
}
}V2 - 람다를 사용해서 중복을 제거한 방식
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class MapMainV2 {
public static void main(String[] args) {
List<String> list = List.of("1", "12", "123", "1234");
List<Integer> numbers = map(list, s -> Integer.valueOf(s));
List<Integer> lengths = map(list, s -> s.length());
}
public static List<Integer> map(List<String> list, Function<String, Integer> function) {
List<Integer> result = new ArrayList<>();
for (String s : list) {
result.add(function.apply(s));
}
return result;
}
}V3 - 별도의 유틸리티 클래스 작성
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class StringToIntegerMapper {
public static List<Integer> map(List<String> list, Function<String, Integer> mapper) {
List<Integer> result = new ArrayList<>();
for (String s : list) {
result.add(mapper.apply(s));
}
return result;
}
}import java.util.List;
public class MapMainV3 {
public static void main(String[] args) {
List<String> list = List.of("1", "12", "123", "1234");
// 문자열을 숫자로 변환
List<Integer> numbers = StringToIntegerMapper.map(list, s -> Integer.valueOf(s));
System.out.println("numbers = " + numbers);
// 문자열의 길이
List<Integer> lengths = StringToIntegerMapper.map(list, s -> s.length());
System.out.println("lengths = " + lengths);
}
}V4 - 제네릭 사용
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class GenericMapper {
public static <T, R> List<R> map(List<T> values, Function<T, R> function) {
List<R> result = new ArrayList<>();
for (T value : values) {
result.add(function.apply(value));
}
return result;
}
}import java.util.List;
public class MapMainV4 {
public static void main(String[] args) {
List<String> fruits = List.of("apple", "banana", "orange");
// String -> String
List<String> upperFruits = GenericMapper.map(fruits, s -> s.toUpperCase());
System.out.println(upperFruits); // [APPLE, BANANA, ORANGE]
// String -> Integer
List<Integer> lengthFruits = GenericMapper.map(fruits, s -> s.length());
System.out.println(lengthFruits); // [5, 6, 6]
// Integer -> String
List<Integer> integers = List.of(1, 2, 3);
List<String> starList = GenericMapper.map(integers, n -> "*".repeat(n));
System.out.println(starList); // [*, **, ***]
}
}람다 활용 - 필터와 매핑
필터와 매핑 예제 1
import java.util.ArrayList;
import java.util.List;
public class Ex1_Number {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
//짝수만 골라서 2배를 반환
List<Integer> result1 = direct(numbers);
List<Integer> result2 = lambda(numbers);
}
//전통적인 방식
private static List<Integer> direct(List<Integer> numbers) {
List<Integer> result = new ArrayList<>();
for (int num : numbers) {
if (num % 2 == 0) {
result.add(num * 2);
}
}
return result;
}
//람다 사용
private static List<Integer> lambda(List<Integer> numbers) {
List<Integer> filtered = GenericFilter.filter(numbers, n -> n % 2 == 0);
return GenericMapper.map(filtered, n -> n * 2);
}
}direct()는 프로그램을 어떻게 수행해야 하는지 수행 절차를 명시한다.
개발자가 로직 하나하나를 어떻게 실행해야 하는지 명시하며, 이러한 프로그래밍 방식을 명령형 프로그래밍이라고 한다.
명령형 스타일은 익숙하고 직관적이지만, 로직이 복잡해질수록 반복 코드가 많아질 수 있다.
반면 lambda()는 무엇을 수행해야 하는지 원하는 결과에 초점을 맞춘다.
특정 조건으로 필터하고 변환하라고 선언하면 구체적인 부분은 내부에서 수행된다.
갭라자는 필터하고 변환하는 것과 같은 무엇을 해야 하는가에 초점을 맞춘다.
이러한 프로그래밍 방식을 선언적 프로그래밍이라고 한다.
선언형 스타일은 무엇을 하고자 하는지가 명확히 드러난다. 따라서 코드 가독성과 유지보수가 쉬워진다.
👆 명령형 vs 선언적 프로그래밍
명령형 프로그래밍
프로그램이 어떻게(How) 수행되어야 하는지, 즉 수행 절차를 명시하는 방식
특징
단계별 실행 : 프로그램의 각 단계를 명확하게 지정하고 순서대로 실행한다.
상태 변화 : 프로그램의 상태(변수 값 등)가 각 단계별로 어떻게 변화하는지 명시한다.
낮은 추상화 : 내부 구현을 직접 제어해야 하므로 추상화 수준이 낮다.
예시 : 전통적인 for 루프, while 루프 등을 명시적으로 사용하는 방식
장점 : 시스템의 상태와 흐름을 세밀하게 제어할 수 있다.
선언적 프로그래밍
프로그램이 무엇을(What) 수행해야 하는지, 즉 원하는 결과를 명시하는 방식
특징
문제 해결에 집중 : 어떻게 문제를 해결할지보다 무엇을 원하는지에 초점을 맞춘다.
코드 간결성 : 간결하고 읽기 쉬운 코드를 작성할 수 있다.
높은 추상화 : 내부 구현을 숨기고 원하는 결과에 집중할 수 있도록 추상화 수준을 높인다.
예시 :
filter,map등 람다의 고차 함수를 활용, HTML, SQL 등장점 : 코드가 간결하고 의미가 명확하며 유지보수가 쉬운 경우가 많다.
필터와 매핑 예제 2
public class Student {
private final String name;
private final int score;
//Constructor, getter, setter, toString...
}import java.util.ArrayList;
import java.util.List;
public class Ex2_Student {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Apple", 100),
new Student("Banana", 80),
new Student("Berry", 50),
new Student("Tomato", 40)
);
//점수가 80점 이상인 학생의 이름을 추출
List<String> direct = direct(students);
List<String> lambda = lambda(students);
System.out.println("direct = " + direct);
System.out.println("lambda = " + lambda);
}
//전통적인 방식
private static List<String> direct(List<Student> students) {
List<String> result = new ArrayList<>();
for (Student student : students) {
if (student.getScore() >= 80) {
result.add(student.getName());
}
}
return result;
}
//람다 사용
private static List<String> lambda(List<Student> students) {
List<Student> filtered = GenericFilter.filter(students, student -> student.getScore() >= 80);
return GenericMapper.map(filtered, student -> student.getName());
}
}람다를 사용하면 구체적으로 어떻게 필터링하고 데이터를 추출하는지 보다는 요구사항에 맞춰서 무엇을 하고 싶은지에 초점을 맞춘다. 결과적으로 코드를 간결하게 작성하고 선언적 스타일로 해결할 수 있다.
람다 활용 - 스트림
필터와 매핑 기능을 별도의 유틸리티 클래스에서 각각 따로 제공하는 방식을 함께 편리하게 사용할 수 있도록 하나의 객체에 기능을 통합해본다.
V1 - 기본
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
public class MyStreamV1 {
private final List<Integer> internalList;
public MyStreamV1(List<Integer> internalList) {
this.internalList = internalList;
}
public MyStreamV1 filter(Predicate<Integer> predicate) {
List<Integer> filtered = new ArrayList<>();
for (Integer element : internalList) {
if (predicate.test(element)) {
filtered.add(element);
}
}
return new MyStreamV1(filtered);
}
public MyStreamV1 map(Function<Integer, Integer> function) {
List<Integer> mapped = new ArrayList<>();
for (Integer element : internalList) {
mapped.add(function.apply(element));
}
return new MyStreamV1(mapped);
}
public List<Integer> toList() {
return this.internalList;
}
}import java.util.List;
public class MyStreamV1Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 짝수만 남기고, 남은 값의 2배를 반환
returnValue(numbers);
methodChain(numbers);
}
//메서드 체인 X
public static void returnValue(List<Integer> numbers) {
MyStreamV1 stream = new MyStreamV1(numbers);
MyStreamV1 filteredStream = stream.filter(n -> n % 2 == 0);
MyStreamV1 mappedStream = filteredStream.map(n -> n * 2);
List<Integer> result = mappedStream.toList();
}
//메서드 체인 O
public static void methodChain(List<Integer> numbers) {
List<Integer> result = new MyStreamV1(numbers).filter(n -> n % 2 == 0)
.map(n -> n * 2)
.toList();
}
}MyStreamV1 클래스의 메서드는 자기 자신의 타입을 반환하기 때문에 메서드 체인 방식을 사용해 깔끔한 구조를 만들 수 있다.
V2 - 정적 팩토리 메서드 추가
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
//static factory 추가
public class MyStreamV2 {
private final List<Integer> internalList;
//private 설정
private MyStreamV2(List<Integer> internalList) {
this.internalList = internalList;
}
//static factory
public static MyStreamV2 of(List<Integer> internalList) {
return new MyStreamV2(internalList);
}
public MyStreamV2 filter(Predicate<Integer> predicate) {
List<Integer> filtered = new ArrayList<>();
for (Integer element : internalList) {
if (predicate.test(element)) {
filtered.add(element);
}
}
return MyStreamV2.of(filtered);
}
public MyStreamV2 map(Function<Integer, Integer> function) {
List<Integer> mapped = new ArrayList<>();
for (Integer element : internalList) {
mapped.add(function.apply(element));
}
return MyStreamV2.of(mapped);
}
public List<Integer> toList() {
return this.internalList;
}
}import java.util.List;
public class MyStreamV2Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 짝수만 남기고, 남은 값의 2배를 반환
List<Integer> result = MyStreamV2.of(numbers)
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.toList();
}
}👆 정적 팩토리 메서드
정적 팩토리 메서드는 객체 생성을 담당하는
static메서드로, 생성자 대신 인스턴스를 생성하고 반환하는 역할을 한다. 즉 일반적인 생성자 대신에 클래스의 인스턴스를 생성하고 초기화하는 로직을 캡슐화하여 제공하는 정적 메서드이다.주요 특징
정적 메서드 : 클래스 레벨에서 호출되며, 인스턴스 생성 없이 접근할 수 있다.
객체 반환 : 내부에서 생성한 객체(또는 이미 존재하는 객체)를 반환한다.
예)
Integer.valueOf()는 미리 생성된 객체를 반환한다.생성자 대체 : 생성자와 달리 메서드 이름을 명시할 수 있어 생성 과정의 목적이나 특징을 명확하게 표현할 수 있다.
유연한 구현 : 객체 생성 과정에서 캐싱, 객체 재활용, 하위 타입 객체 반환 등 다양한 로직을 적용할 수 있다.
생성자는 이름을 부여할 수 없지만, 정적 팩토리 메서드는 의미있는 이름을 부여할 수 있기 때문에 가독성이 더 좋아지는 장점이 있다. 인자들을 받아 간단하게 객체를 생성할 때는 주로
of(...)라는 이름을 사용한다.하지만 반대로 보면 이름도 부여해야 하고 준비해야 하는 코드도 더 많다. 객체의 생성이 단순한 경우에는 생성자를 직접 사용하는 것이 더 나은 선택일 수 있다.
V3 - 제네릭 사용 + 기능 추가
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
//Generic 추가
public class MyStreamV3<T> {
private final List<T> internalList;
private MyStreamV3(List<T> internalList) {
this.internalList = internalList;
}
//static factory
public static <T> MyStreamV3<T> of(List<T> internalList) {
return new MyStreamV3<>(internalList);
}
public MyStreamV3<T> filter(Predicate<T> predicate) {
List<T> filtered = new ArrayList<>();
for (T element : internalList) {
if (predicate.test(element)) {
filtered.add(element);
}
}
return MyStreamV3.of(filtered);
}
public <R> MyStreamV3<R> map(Function<T, R> function) {
List<R> mapped = new ArrayList<>();
for (T element : internalList) {
mapped.add(function.apply(element));
}
return MyStreamV3.of(mapped);
}
//추가
public void forEach(Consumer<T> consumer) {
for (T element : internalList) {
consumer.accept(element);
}
}
public List<T> toList() {
return this.internalList;
}
}import java.util.List;
public class MyStreamV3Main {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Apple", 100),
new Student("Banana", 80),
new Student("Berry", 50),
new Student("Tomato", 40)
);
//점수가 80점 이상인 학생의 이름을 추출
List<String> result1 = MyStreamV3.of(students)
.filter(student -> student.getScore() >= 80)
.map(student -> student.getName())
.toList();
//점수가 80점 이상이면서, 이름이 5글자인 학생의 이름을 대문자로 추출
List<String> result2 = MyStreamV3.of(students)
.filter(student -> student.getScore() >= 80)
.filter(student -> student.getName().length() == 5)
.map(student -> student.getName())
.map(name -> name.toUpperCase())
.toList();
//외부 반복
for (String name : result1) {
System.out.println("name = " + name);
}
//내부 반복
MyStreamV3.of(students)
.filter(student -> student.getScore() >= 80)
.map(student -> student.getName())
.forEach(name -> System.out.println("name = " + name));
}
}👆 외부 반복 vs 내부 반복
외부 반복
for문,while문과 같은 반복문을 직접 사용해서 데이터를 순회하는 방식개발자가 직접 각 요소를 반복하며 처리한다.
내부 반복
직접 반복 제어문을 작성하지 않고, 반복 처리를 스트림 내부에 위임하는 방식
스트림 내부에서 요소들을 순회하고, 개발자는 처리 로직(람다)만 정의해주면 된다.
코드가 훨씬 간결해지며, 선언형 프로그래밍 스타일을 적용할 수 있다.
내부 반복 방식은 반복의 제어를 스트림에게 위임하기 때문에 코드가 간결해진다. 즉 개발자는 어떤 작업을 할지를 집중적으로 작성하고, 어떻게 순회할지는 스트림이 담당하도록 하여 생산성과 가독성을 높일 수 있다.
많은 경우 내부 반복이 선언형 프로그래밍 스타일로 더욱 직관적이기 때문에 더 나은 선택이다. 다만 때때로 외부 반복을 선택하는 것이 더 나은 경우도 있다.
단순히 한두 줄 수행만 필요한 경우
반복 제어에 대한 복잡하고 세밀한 조정이 필요한 경우 (
break나continue등을 사용하는 경우)
Last updated