It's going to be one day 🍀

안녕하세요! 매일 매일 공부하려고 노력하는 백엔드 개발자 지망생의 공부 흔적입니다.

Back-End/Java # 교육

[Java 교육] 람다식 마무리/스트림 (다시 복습 필수)

2jin2 2024. 2. 19. 16:51

[공부 내용 정리]

java.util.function 패키지 : 함수형 인터페이스 제공

Runnable

  • 매개변수와 리턴 값 모두 없는 경우
package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

아래 예시처럼 매개변수와 리턴값(타입)이 없는 람다식을 참조 가능함.

Runnable r = () -> System.out.println("출력문 테스트");
r.run();    // "출력문 테스트" 

Supplier<T>

  • 매개변수는 없고, 리턴값(타입)이 있습니다.
package java.util.function;

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

아래 예시처럼 매개변수가 없고, 리턴타입이 있는 람다식을 참조 가능함.

Supplier<String> s = **() -> "리턴되는 값";**
String result = s.get();
System.out.println(result);   // "리턴되는 값"

Consumer<T>

  • Supplier와 반대로, 매개변수는 있지만 리턴타입이 음.
  • 매개변수는 있지만 리턴타입이 없는 람다식을 참조 가능함.
package java.util.function;

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
Consumer<String> c = (a) -> System.out.println(a);
c.accept("consumer");

Function<T, R>

  • 하나의 매개변수를 받아서 하나의 결과를 리턴함.
  • 매개변수를 받아서 하나의 결과를 리턴하는 람다식을 참조 가능함.
package java.util.function;

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Function<Integer, String> f = a -> String.valueOf(a);
Function<String, Integer> f2 = b -> {
    return Integer.valueOf(b) + 100;
};

Predicate<T>

  • 조건식을 표현하는데 사용됨.
  • 매개변수는 하나, 리턴타입은 boolean을 갖는 함수형 인터페이스.
package java.util.function;

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

하나의 매개변수, 리턴타입이 boolean인 람다식을 참조함.

Predicate<String> isEmptyStr = s -> s.length()==0;
String str = "";
if (isEmptyStr.test(str)) { // if(s.length()==0)
		System.out.println("This is an empty String.")
}
// 스트림에서 filter메소드 내부에는 Predicate 타입이 들어감
List<Integer> list = Arrays.asList(1,2,3,4,5);
list.stream()
		.filter(x -> x%2==0)
		.collect(Collectors.toList()); // [2,4] 

문제) LongSupplier 함수형 인터페이스로 참조가 가능한

@FunctionalInterface
// Output만 존재
public interface LongSupplier {
    long getAsLong();
}
LongSupplier ls =() -> {
            long b = 34L;
            return b;
        };
        System.out.println(ls.getAsLong());

메소드 참조

Method Reference : 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내서, 람다식에서 불필요한 매개 변수를 제거함.

(left, right) -> Math.max(left, right);

 

 

Math::max;   // 메소드 참조

-> 이 코드를 설명하면, left, right 두 개의 값을 Math.max() 메소드의 매개값으로 전달하는 역할을 함. 메소드 참조를 이용하면 이렇게 깔끔하게 처리할 수 있음!

 

IntBinaryOperator 인터페이스 예시)

@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}
// Math.max 메소드
public static int max(int a, int b) {
    return Math.max(a, b); 
}
람다식을 '메소드참조'로 더욱 간단하게 변경해가는 과정

// 1단계
IntBinaryOperator operator = (a, b) -> Math.max(a, b);

// 2단계
IntBinaryOperator operator = Math::max;   // 메소드 참조

 

정적 메소드 및 인스턴스 메소드 참조

정적(static) 메소드를 참조할 경우에는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메소드 이름을 쓰면 됨.

클래스::메소드 

인스턴스 메소드일 경우에는 먼저 객체를 생성한 다음, 참조 변수 뒤에 :: 기호를 붙이고 인스턴스 메소드 이름을 쓰면 됨.

참조변수::메소드 

 

ex) Calculator의 정적 및 인스턴스 메소드 참조

public class MethodReferenceExample {
    public static void main(String[] args) {
        IntBinaryOperator operator;

        // 정적 메소드 참조
        operator = (x, y) -> Calculator.staticMethod(x, y);
        System.out.println("결과1: " + operator.applyAsInt(1, 2));

        operator = Calculator::staticMethod;
        System.out.println("결과2: " + operator.applyAsInt(3, 4));

        // 인스턴스 메소드 참조
        Calculator calculator = new Calculator();
        operator = (x, y) -> calculator.instanceMethod(x, y);
        System.out.println("결과3: " + operator.applyAsInt(5, 6));

        operator = calculator::instanceMethod;
        System.out.println("결과4: " + operator.applyAsInt(7, 8));
    }
}
public class Calculator {
    public static int staticMethod(int x, int y) {   // 정적 메소드
        return x + y;
    }

    public int instanceMethod(int x, int y) {   // 인스턴스 메소드
        return x + y;
    }
}

 

매개변수의 메소드 참조

람다식에서 제공되는 a 매개변수의 메소드를 호출해서 b 매개변수를 사용하는 경우

(a, b) -> { a.instanceMethod(b); }

이것을 메소드로 표현하면 a클래스 이름 뒤에 :: 기호를 붙이고, 메소드 이름을 기술하면 됨.

클래스::instanceMethod

 

ex) 두 문자열이 대소문자와 상관없이 동일한 알파벳으로 구성되었는지 비교하는 로직 (String의 인스턴스 메소드인 compareTolgnoreCase()) 사용.

  • a.compareTolgnoreCase(b)로 호출될 때 사전 순서대로 a가 b보다 먼저 오면 음수, 동일하면 0, a가 b보다 나중에 오면 양수를 리턴함.
package chapter12;

import java.util.function.ToIntBiFunction;

public class ArgumentMethodReferencesExample {
    public static void main(String[] args) {
        ToIntBiFunction<String, String> function;

        function = (a, b) -> a.compareToIgnoreCase(b);
        print(function.applyAsInt("Java8", "JAVA8"));

        function = String::compareToIgnoreCase;
        print(function.applyAsInt("Java8", "JAVA8"));
    }

    public static void print(int order) {
        if (order < 0) {
            System.out.println("사전순으로 먼저 옵니다.");
        } else if (order == 0) {
            System.out.println("동일한 문자열");
        } else {
            System.out.println("사전순으로 나중에 옵니다.");
        }
    }
}

→ 실행 결과) 동일한 문자열, 동일한 문자열

 

생성자 참조

메소드 참조(method references)는 생성자 참조도 포함함. 생성자를 참조한다는 것은 객체를 생성하는 것을 의미함.

다음 코드를 보면 람다식은 단순히 객체 생성 후 리턴만 함.

(a, b) -> { return new 클래스(a, b); }

이 경우 생성자 참조로 표현하면 다음과 같음.

클래스 :: new

스트림

스트림(stream)은 데이터의 흐름이라고 할 수 있음. 자바 8부터 사용할 수 있는 기능이며 배열이나 컬렉션을 가공하여 원하는 결과를 얻을 수 있음.

// Collection 인터페이스의 메서드로 제공
Stream<E> stream()

문제) List의 숫자들 중에 짝수만 출력하는 코드

ex) 스트림을 사용하지 않았을 때

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

for (int n: numbers) {
	if (n % 2 == 0) {
		System.out.println(n);
	}
}

ex) 스트림을 사용했을 때

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.stream()
	.filter(n -> n % 2 == 0)
	.forEach(System.out::println);

 

스트림의 종류

java.util.stream 패키지에 스트림 인터페스가 정의되어있음.

 

스트림을 만드는 방법

 

1. 컬렉션으로부터 스트림 생성

문자열 리스트로 스트림을 생성하는 예시 

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

Stream<String> stream = list.stream();
stream.forEach(System.out::println);

 

2.  배열로부터 스트림 생성

String[] arr = new String[]{"a", "b", "c", "d", "e"};

Stream<String> stream = Arrays.stream(arr);
stream.forEach(System.out::println);
public class StreamExample2 {
    public static void main(String[] args) {
        String[] arr = {"첫번째", "두번째"};
        Stream<String> stream = Arrays.stream(arr);
        stream.forEach(System.out::println); // 출력 
    }
}

 

3. 숫자 범위로부터 스트림 생성

IntStream intStream = IntStream.range(1, 5);	// [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5);	// [1, 2, 3, 4, 5]

 

Random 클래스로 스트림을 생성할 수도 있음. 아래는 난수 5개로 스트림을 생성하는 예시임

DoubleStream doubleStream = new Random().doubles(5); // 난수 5개 생성

 

4. 파일로부터 스트림 생성

보류

 

5. 스트림 연결

Stream.concat 메소드를 이용해 두 개의 스트림을 연결할 수 있음.

Stream<Integer> stream1 = Stream.of(1, 2, 3);
Stream<Integer> stream2 = Stream.of(4, 5, 6);

Stream<Integer> newStream = Stream.concat(stream1, stream2);
newStream.forEach(System.out::println); // 1, 2, 3, 4, 5, 6

 

스트림 가공

 

distinct

중복 제거

List<String> list = Arrays.asList("a", "b", "b", "c", "d");
        list.stream().distinct().forEach(System.out::println);

→ 실행 결과) a, b, c, d

 

filter

Predicate가 true 인 것만 필터링함. Predicate란 매개변수를 받아 boolean값을 반환하는 함수임. 즉, 조건이 참이면 true, 거짓이면 false를 리턴함.

ex) “김”으로 시작하는 문자열만 골라서 출력하는 코드

List<String> list = Arrays.asList("김밥", "김밥", "김치", "나비", "나방");

// [김밥, 김밥, 김치]
list.stream()
	.filter(str -> str.startsWith("김"))
	.forEach(System.out::println);

 

아래 예시처럼 distinct와 filter를 동시에 적용할 수도 있음!

List<String> list = Arrays.asList("김밥", "김밥", "김치", "나비", "나방");

// [김밥, 김치]
list.stream()
	.filter(str -> str.startsWith("김"))
	.distinct()
	.forEach(System.out::println);

Daily Quiz

1. 람다식에 대한 설명으로 틀린것은?

  1. 람다식은 함수형 인터페이스의 익명 구현 객체를 생성한다.
  2. 매개 변수가 없을 경우 ( ) → { … } 형태로 작성한다.
  3. (x, y) → { return x + y; }는 (x, y) → x + y;로 바꿀 수 있다.
  4. @FunctionalInterface가 기술된 인터페이스만 람다식으로 표현이 가능하다. X

2. 메소드 참조에 대한 설명으로 틀린 것은?

  1. 메소드 참조는 함수적 인터페이스의 익명 구현 객체를 생성한다.
  2. 인스턴스 메소드는 “참조변수::메소드”로 기술한다.
  3. 정적 메소드는 “클래스::메소드”로 기술한다.
  4. 생성자 참조인 “클래스::new”는 매개 변수가 없는 디폴트 생성자만 호출한다. X → 다른 생성자도 호출할 수 있음.

3. 잘못 작성한 람다식은?

  1. a → a + 3
  2. a, b → a * b X -> 두 개 이상일 때는  앞에서 괄호를 쳐줘야 함. (a, b)
  3. x → System.out.println(x/5)
  4. (x, y) → Math.max(x, y)

4. 다음 코드는 컴파일 에러가 발생합니다. 그 이유는?

public class LambdaExample {
    public static int method(int x, int y) {
        IntSupplier supplier = () -> {
            x *= 10;
            int result = x + y;
            return result;
        };
        int result = supplier.getAsInt();
        return result;
    }

    public static void main(String[] args) {
        System.out.println(method(3, 5));
    }
}

→ 람다식 안에서는 외부 메소드의 매개변수를 final로 취급해서 변수 할당과 수정이 불가능하기 때문이다.

한 마디로, 람다식 외부의 변수는 final로 취급한다.

5. 다음은 배열 항목 중 최대값 또는 최소값을 찾는 코드입니다. maxOrMin() 메소드의 매개값을 람다식으로 기술해보세요.

public class LambdaExample_5 {
    private static int[] scores = {10, 50, 3};

    public static int maxOrMin(IntBinaryOperator operator) {
        int result = scores[0];
        for (int score : scores) {
            result = operator.applyAsInt(result, score);
        }
        return result;
    }

    public static void main(String[] args) {
        int max = maxOrMin(
                /* 최대값 얻기 구현 */
        );
        System.out.println("최대값 : " + max);
        
        int min = maxOrMin(
                /* 최소값 얻기 구현 */
        );
        System.out.println("최소값: " + min);
    }
}
최대값 : 50
최소값: 3

→ 코드 두 개 다 가능!

public static void main(String[] args) {
        int max = maxOrMin(Math::max);
        System.out.println("최대값 : " + max);
        
        int min = maxOrMin(Math::min);
        System.out.println("최소값: " + min);
    }
public static void main(String[] args) {
        int max = maxOrMin((a, b) -> a > b ? a : b);
        System.out.println("최대값 : " + max);
        
        int min = maxOrMin((a, b) -> a < b ? a : b);
        System.out.println("최소값: " + min);
    }

6. 다음은 학생의 영어 평균 점수와 수학 평균 점수를 계산하는 코드입니다. avg() 메소드를 선언해보세요.

public class LambdaExample_6 {
    private static Student[] students = {
            new Student("홍길동", 90, 96),
            new Student("저팔계", 95, 93)
    };

    public static void main(String[] args) {
        	/*  avg() 메소드 작성
    */
    }
    
    public static class Student {
        private String name;
        private int englishScore;
        private int mathScore;

        public Student(String name, int englishScore, int mathScore) {
            this.name = name;
            this.englishScore = englishScore;
            this.mathScore = mathScore;
        }

        public String getName() {
            return name;
        }

        public int getEnglishScore() {
            return englishScore;
        }

        public int getMathScore() {
            return mathScore;
        }
    }
}

public static void main(String[] args) {
    LambdaExample_6 example = new LambdaExample_6();
    double englishAvg = example.avg( s -> s.getEnglishScore() );
    System.out.println("영어 평균 점수: " + englishAvg);

    double mathAvg = example.avg( s -> s.getMathScore() );
    System.out.println("수학 평균 점수: " + mathAvg);
}

7. 6번의 main() 메소드에서 avg() 호출할 때 매개값으로 준 람다식을 메소드 참조로 변경해보세요.

double englishAvg = average( s -> s.getEnglishScore() );
-> double englishAvg = avg (      );

double mathAvg = average( s -> s.getMathScore() );
-> double mathAvg = avg (        );

public static void main(String[] args) {
    LambdaExample_6 example = new LambdaExample_6();
    double englishAvg = example.avg(Student::getEnglishScore);
    System.out.println("영어 평균 점수: " + englishAvg);

    double mathAvg = example.avg(Student::getMathScore);
    System.out.println("수학 평균 점수: " + mathAvg);
}