9일차 - Lambda

은채의 성장통·2025년 6월 10일

KCC정보통신

목록 보기
13/30

노션정리

1. 람다 표현식

자바에서는 함수를 값처럼 직접 전달할 수 없습니다.

즉, exe("hello", func)와 같은 형태는 지원되지 않으며,

반드시 메서드 실행 결과 (func())를 인자로 전달해야 합니다.

그러나 람다 표현식과 함수형 인터페이스를 사용하면 동작(메서드)을 변수처럼 다룰 수 있으며, 인자로 전달하는 것이 가능합니다.

이를 통해 유지보수성과 확장성을 높일 수 있습니다.

1.0 람다 표현식 개요

람다 표현식(Lambda Expression)은 메서드처럼 기능을 전달하는 방식을 제공하여 코드를 간결하게 만들고, 함수형 프로그래밍 스타일을 지원합니다. 이를 활용하면 특정 동작을 하나의 식으로 표현할 수 있습니다.


1.1 람다 표현식

람다 표현식은 다음과 같은 형식으로 작성됩니다.

(parameter1, parameter2, ...) -> { body }
  • 메서드를 선언하지 않고도 기능을 전달할 수 있음
  • 코드가 간결하며, 함수형 인터페이스와 함께 사용됨

문자열 배열 정렬 예제

Arrays.sort()의 두 번째 인자로 람다 표현식을 전달하여 문자열의 길이를 기준으로 정렬합니다.

package lambda;

import java.util.Arrays;

public class LambdaExample {
    public static void main(String[] args) {
        // 정렬할 문자열 배열
        String[] names = {"Alice", "Bob", "Charlie", "David"};

        // 람다 표현식을 사용하여 문자열 길이순으로 정렬
        Arrays.sort(names, (a, b) -> a.length() - b.length());

        // 정렬된 배열 출력
        for (String name : names) {
            System.out.println(name);
        }
    }
}
  • Arrays.sort(names, (a, b) -> a.length() - b.length());
  • a.length() - b.length() → 문자열 길이 차이를 반환하여 정렬

1.2 함수형 인터페이스와 람다식

람다식을 사용하려면 함수형 인터페이스(FunctionalInterface)가 필요합니다.

함수형 인터페이스는 추상 메서드가 하나만 존재하는 인터페이스입니다.

함수형 인터페이스 선언

package lambda;

@FunctionalInterface
interface MyFunction {
    void performAction(String message); // 단 하나의 추상 메서드
}
  • @FunctionalInterface → 컴파일러가 함수형 인터페이스임을 보장
  • performAction(String message); → 람다식에서 구현할 메서드

람다 표현식을 활용한 함수 실행

람다식을 사용하여 MyFunction을 구현하고 특정 동작을 실행합니다.

package lambda;

public class LambdaExample2 {
    // 함수형 인터페이스를 인자로 받는 메서드
    public static void executeAction(String message, MyFunction function) {
        function.performAction(message);
    }

    public static void main(String[] args) {
        // 람다식을 사용하여 함수형 인터페이스 구현
        MyFunction myFunction = (message) -> System.out.println("Action performed: " + message);

        // 함수형 인터페이스를 인자로 갖는 메서드 호출
        executeAction("Hello, world!", myFunction);
    }
}
  • executeAction(String message, MyFunction function) → 함수형 인터페이스를 매개변수로 받음
  • myFunction = (message) -> System.out.println("Action performed: " + message); → 람다식 구현
  • executeAction("Hello, world!", myFunction); → 람다식을 메서드에 전달하여 실행

실행 결과

Action performed: Hello, world!

1.3 자바의 함수형 인터페이스

자바에서는 함수형 인터페이스를 활용하여 람다식을 사용하고, 이를 인자로 전달할 수 있습니다.

1. 함수형 인터페이스 정의

package lambda;

@FunctionalInterface   //- 함수형 인터페이스이므로, 람다식을 사용할 수 있습니다. 하나만의 메서드만 만들수 있음
interface MyFunction {
    void performAction(String message);  //메서드 1개
		//void a(int b);  이거 하면 오류남 람다식에는 메서드가 1개여야하는데 위에 일단 1개로 강제하기도 했서
}
  • @FunctionalInterface를 사용하여 함수형 인터페이스임을 명시
  • performAction(String message);람다식에서 구현할 메서드

2. 람다식을 활용한 함수형 인터페이스 사용

package lambda;

public class LambdaExample2 {
    // 함수형 인터페이스를 인자로 받는 메서드 정의
    public static void executeAction(String message, MyFunction function) {
        function.performAction(message);
    }

    public static void main(String[] args) {
        // 람다식을 사용하여 함수형 인터페이스 구현
        MyFunction myFunction = (message) -> System.out.println("Action performed: " + message);

        // 함수형 인터페이스를 인자로 갖는 메서드 호출
        executeAction("Hello, world!", myFunction);
    }
}

출력 결과

Action performed: Hello, world!
  • executeAction(String message, MyFunction function)함수형 인터페이스를 인자로 받음
  • myFunction람다식을 사용하여 메서드 구현
  • executeAction()에서 함수형 인터페이스를 실행

1.4 Function 인터페이스와 람다식

자바에서 제공하는 기본 함수형 인터페이스를 사용하면 다양한 형태의 함수 표현이 가능합니다.

함수형 인터페이스 정리

인터페이스설명메서드예제
Supplier매개변수 없이 결과 값을 제공T get()데이터 소스로부터 값을 제공
Consumer매개변수를 받아서 소비void accept(T t)리스트의 각 요소 출력
Function<T, R>하나의 인자를 받아 결과 반환R apply(T t)문자열을 정수로 변환
Predicate주어진 조건을 평가boolean test(T t)리스트에서 특정 조건을 만족하는 요소 필터링
UnaryOperator하나의 인자를 받아 같은 유형의 결과 반환T apply(T t)숫자를 제곱하는 등의 동작
BinaryOperator두 개의 인자를 받아 같은 유형의 결과 반환T apply(T t1, T t2)두 숫자를 더하거나 두 문자열을 연결

정리

  • 함수형 인터페이스를 활용하여 람다식을 인자로 전달할 수 있음
  • 기본 제공 함수형 인터페이스를 사용하면 다양한 함수 표현 가능
  • 유지보수성과 확장성이 높은 코드 작성 가능
package lambda;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

/**
 * MyUtil 클래스
 * 이 클래스는 제네릭 타입 <T>를 사용하여 다양한 데이터 유형을 처리할 수 있음.
 * filter() 메서드는 주어진 조건(Predicate 인터페이스)을 충족하는 요소만 리스트에 포함하여 반환함.
 */
public class MyUtil<T> { // 제네릭 타입 <T>를 지정한 클래스 선언
    /**
     * filter() 메서드
     * @param objects 필터링할 대상 리스트 (List<T>)
     * @param pred 필터링 조건을 정의하는 함수형 인터페이스 Predicate<T>
     * @return 조건을 만족하는 요소만 포함한 새로운 리스트 반환 (List<T>)
     */
    public List<T> filter(List<T> objects, Predicate<T> pred){
        List<T> output = new ArrayList<>(); // 필터링된 결과를 저장할 리스트 생성
        
        // 리스트의 모든 요소에 대해 반복
        for(T obj : objects) { 
            // Predicate<T>.test(obj) 호출 -> 주어진 조건을 만족하는지 검사
            if(pred.test(obj)) { 
                output.add(obj); // 조건을 만족하면 output 리스트에 추가
            }
        }
        
        return output; // 필터링된 리스트 반환
    }
}

package lambda;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class LambdaExample0 {
    public static void main(String[] args) {
        // Step 1: 정수 리스트 생성 (Arrays.asList 사용)
        List<Integer> intList = Arrays.asList(10, 15, 23, 15, 3125, 2);

        // Step 2: MyUtil<Integer> 객체 생성 (제네릭 활용)
        MyUtil<Integer> util = new MyUtil<>(); // MyUtil 클래스의 제네릭 인스턴스 생성

        // Step 3: 람다 표현식을 이용하여 필터링 (짝수만 포함)
        List<Integer> filteredOutput = util.filter(intList, (data) -> data % 2 == 0);
        System.out.println(filteredOutput); // 출력: [10, 2]

        // Step 4: 익명 클래스를 이용하여 필터링 (짝수만 포함)
        List<Integer> filteredOutput2 = util.filter(intList, new Predicate<Integer>() {
            @Override
            public boolean test(Integer data) {
                return data % 2 == 0; // 짝수만 반환하는 조건
            }
        });
        System.out.println(filteredOutput2); // 출력: [10, 2]

        // Step 5: 문자열 리스트 생성
        List<String> strList = Arrays.asList("안녕", "ㅁㄴㅇ", "ㅂㅈㅎㅂㅈㄹ", "ㅍㄴㅇㅍ");

        // Step 6: MyUtil<String> 객체 생성 (제네릭 활용)
        MyUtil<String> util2 = new MyUtil<>();

        // Step 7: 람다 표현식을 이용하여 필터링 (문자 길이가 5 이상인 경우)
        List<String> filteredOutput3 = util2.filter(strList, (data) -> data.length() >= 5);
        System.out.println(filteredOutput3); // 출력: ["ㅂㅈㅎㅂㅈㄹ"]
    }
}

1.5 메서드 및 생성자 참조

람다 표현식을 더욱 간결하게 작성할 수 있도록 메서드 참조 연산자(::)를 사용하면 기존 메서드나 생성자를 직접 참조할 수 있습니다.


1.5.1 정적 메서드 참조

  • 클래스의 정적 메서드를 참조하여 람다식 없이 호출하는 방식입니다.
  • 형식: 클래스명::메서드명
package lambda;
import java.util.function.Function;

public class MethodReferenceExample1 {
    // 정적 메서드
    public static String toUpperCase(String str) {
        return str.toUpperCase();
    }

    public static void main(String[] args) {
        // 람다 표현식을 사용한 방식
        Function<String, String> lambda = (str) -> toUpperCase(str);

        // 메서드 참조를 사용한 방식
        Function<String, String> reference = MethodReferenceExample1::toUpperCase;

        System.out.println(lambda.apply("hello"));  // 출력: HELLO
        System.out.println(reference.apply("world")); // 출력: WORLD
    }
}
  • Function<String, String>은 입력값을 받아 변환 후 반환하는 함수형 인터페이스
  • apply("hello")Function에서 제공하는 메서드로, 값을 전달하여 함수 실행

1.5.2 인스턴스 메서드 참조

  • 인스턴스 메서드를 참조하여 람다식 없이 호출하는 방식입니다.
  • 형식: 객체변수명::메서드명
package lambda;
import java.util.function.Function;

public class MethodReferenceExample2 {
    // 인스턴스 메서드
    public String toUpperCase(String str) {
        return str.toUpperCase();
    }

    public static void main(String[] args) {
        MethodReferenceExample2 instance = new MethodReferenceExample2();

        // 람다 표현식을 사용한 방식
        Function<String, String> lambda = (str) -> instance.toUpperCase(str);

        // 메서드 참조를 사용한 방식
        Function<String, String> reference = instance::toUpperCase;

        System.out.println(lambda.apply("hello"));  // 출력: HELLO
        System.out.println(reference.apply("world")); // 출력: WORLD
    }
}
  • apply("hello")Function에서 람다식이나 메서드 참조를 통해 값을 변환
  • instance::toUpperCase를 사용하면 기존 객체의 메서드를 참조하여 실행

1.5.3 생성자 참조

  • 클래스의 생성자를 참조하여 객체를 생성하는 방식입니다.
  • 형식: 클래스명::new
package lambda;
import java.util.function.Supplier;

public class MethodReferenceExample3 {
    // 생성자
    public MethodReferenceExample3() {
        System.out.println("Constructor called");
    }

    public static void main(String[] args) {
        // 람다 표현식을 사용한 방식
        Supplier<MethodReferenceExample3> lambda = () -> new MethodReferenceExample3();

        // 생성자 참조를 사용한 방식
        Supplier<MethodReferenceExample3> reference = MethodReferenceExample3::new;

        lambda.get();   // 생성자 호출
        reference.get(); // 생성자 호출
    }
}
  • Supplier<MethodReferenceExample3>은 매개변수가 없는 객체를 반환하는 함수형 인터페이스
  • get()Supplier에서 제공하는 메서드로, 객체를 생성하여 반환

apply()와 get()의 차이

메서드명사용 인터페이스기능
apply()Function<T, R>입력값(T)을 받아 변환 후 반환(R)
get()Supplier<T>입력값 없이 객체를 반환

apply()는 입력값을 받지만, get()은 입력값 없이 호출 가능하다는 차이가 있습니다.


2. 스트림 API

자바의 스트림(Stream) API는 컬렉션(리스트, 셋, 맵 등)과 배열 같은 데이터 소스를 효율적으로 처리하는 기능을 제공합니다. 이를 활용하면 가독성이 높은 코드 작성이 가능하며, 선언적 스타일로 데이터를 다룰 수 있습니다.


2.1. 스트림 생성

2.1.1 컬렉션에서 스트림 생성

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

2.1.2 배열에서 스트림 생성

String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);

2.1.3 스트림 빌더 사용

Stream<String> stream = Stream.<String>builder()
    .add("a")
    .add("b")
    .add("c")
    .build();

2.1.4 Stream.of() 이용

Stream<String> stream = Stream.of("a", "b", "c");

2.2. 중간 연산

중간 연산은 스트림을 변환하며, 최종 연산이 호출되기 전까지 실제로 실행되지 않습니다.

  • filter: 특정 조건을 만족하는 요소만 포함
    stream.filter(s -> s.startsWith("a"));
    
  • map: 각 요소를 다른 값으로 변환
    stream.map(String::toUpperCase);
    
  • flatMap: 중첩된 리스트를 평탄화하여 하나의 스트림으로 변환
    List<List<String>> list = Arrays.asList(
        Arrays.asList("a"), Arrays.asList("b", "c"), Arrays.asList("d")
    );
    Stream<String> flatStream = list.stream().flatMap(Collection::stream);
    
  • distinct: 중복 요소 제거
    stream.distinct();
    
  • sorted: 요소 정렬
    stream.sorted();
    
  • peek: 스트림의 각 요소를 소비하면서 원본 스트림 반환
    stream.peek(System.out::println);
    
  • limit(n): 최대 n개 요소 포함
    stream.limit(2);
    
  • skip(n): 처음 n개 요소 제외
    stream.skip(1);
    

2.3. 최종 연산

최종 연산은 스트림을 소비하며, 값을 반환하거나 컬렉션을 생성합니다.

  • forEach: 모든 요소에 대해 특정 동작 수행
    stream.forEach(System.out::println);
    
  • collect: 결과를 컬렉션으로 변환
    List<String> resultList = stream.collect(Collectors.toList());
    
  • reduce: 요소를 하나의 값으로 결합
    Optional<String> concatenated = stream.reduce((s1, s2) -> s1 + s2);
    
  • count: 요소 수 반환
    long count = stream.count();
    
  • anyMatch, allMatch, noneMatch: 조건 만족 여부 확인
    boolean anyStartsWithA = stream.anyMatch(s -> s.startsWith("a"));
    boolean allStartWithA = stream.allMatch(s -> s.startsWith("a"));
    boolean noneStartWithA = stream.noneMatch(s -> s.startsWith("a"));
    
  • findFirst, findAny: 첫 번째 또는 임의의 요소 반환
    Optional<String> first = stream.findFirst();
    Optional<String> any = stream.findAny();
    

1. StreamOperationExample 클래스

이 클래스는 기본적인 스트림 연산을 수행하는 예제입니다. 여기서 순회, 정렬, 필터링, 매핑, 집계, 그룹핑을 적용합니다.

코드 정리 및 설명

package stream;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class StreamOperationExample {
    public static void main(String[] args) {
        // 리스트 생성 (1~10)
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // 1. 순회 (각 요소 출력)
        System.out.println("순회:");
        numbers.stream().forEach(System.out::println);

        // 2. 정렬 (오름차순)
        System.out.println("\n정렬:");
        numbers.stream().sorted().forEach(System.out::println);

        // 3. 필터링 (짝수만 출력)
        System.out.println("\n짝수 필터링:");
        numbers.stream().filter(n -> n % 2 == 0).forEach(System.out::println);

        // 4. 매핑 (각 숫자의 제곱값으로 변환)
        System.out.println("\n제곱 값 매핑:");
        numbers.stream().map(n -> n * n).forEach(System.out::println);

        // 5. 집계 (합계 계산)
        System.out.println("\n합계:");
        int sum = numbers.stream().reduce(0, Integer::sum);
        System.out.println(sum);

        // 6. 그룹핑 (3으로 나눈 나머지 기준으로 그룹화)
        System.out.println("\n나머지에 따른 그룹핑:");
        Map<Integer, List<Integer>> groupByRemainder = numbers.stream()
                .collect(Collectors.groupingBy(n -> n % 3));
        System.out.println(groupByRemainder);
    }
}

핵심 개념

  • 순회 (forEach): 모든 요소를 하나씩 반복하며 출력.
  • 정렬 (sorted): 스트림의 요소를 정렬 (기본 오름차순).
  • 필터링 (filter): 조건을 만족하는 요소만 포함.
  • 매핑 (map): 각 요소를 새로운 값으로 변환.
  • 집계 (reduce): 요소를 하나의 값으로 합침.
  • 그룹핑 (groupingBy): 특정 기준으로 요소를 그룹화.

2. Student 클래스

학생 정보를 저장하는 간단한 데이터 클래스입니다.

코드 정리 및 설명

package stream;

public class Student {
    private String name;
    private int score;

    // 생성자 (이름과 점수를 초기화)
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    // 이름을 반환하는 메서드
    public String getName() {
        return name;
    }

    // 점수를 반환하는 메서드
    public int getScore() {
        return score;
    }
}

핵심 개념

  • 데이터 클래스로, namescore를 멤버 변수로 가짐.
  • 생성자를 이용해 객체 생성 시 값을 초기화.
  • *Getter 메서드 (getName, getScore)**를 통해 필드 값을 반환.

3. StreamExample 클래스

이 클래스는 학생 데이터를 스트림을 이용해 처리하는 예제입니다.

코드 정리 및 설명

package stream;

import java.util.Arrays;
import java.util.List;
import java.util.OptionalDouble;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        // 학생 리스트 생성
        List<Student> students = Arrays.asList(
                new Student("Alice", 85),
                new Student("Bob", 92),
                new Student("Charlie", 78),
                new Student("Dave", 88),
                new Student("Eve", 95)
        );

        // 1. 평균 점수 계산
        OptionalDouble averageScore = students.stream()
                .mapToInt(Student::getScore)
                .average();

        averageScore.ifPresent(avg -> System.out.println("Average score: " + avg));

        // 2. 90점 이상인 학생들의 이름 추출
        List<String> topStudents = students.stream()
                .filter(s -> s.getScore() >= 90) // 조건: 90점 이상
                .map(Student::getName) // 이름으로 변환
                .collect(Collectors.toList()); // 리스트로 수집

        System.out.println("Top students: " + topStudents);
    }
}
profile
인생 별거 없어

0개의 댓글