Search
Duplicate
📒

[Java Study] 10-2. 람다와 스트림

상태
미진행
수업
Java Study
주제
Collection
4 more properties
참고

람다

NOTE
람다는 식별자 없이 실행 가능한 함수로 문법이 간결해져 사용이 매우 편리해진다!
// 익명 클래스 코드 Collections.sort(words, new Comparator<String>() { public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); } }); // 람다 코드(훨씬 단축됨!) Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
Java
복사
람다 예제
람다의 매개변수는 타입을 명시해야 할 때를 제외하고는 생략하는것이 좋다.
타입추론에 관련해서 제네릭을 통해 정보를 얻으므로 제네릭을 잘 사용해야 한다.

람다 와 익명클래스

람다
함수 객체를 구현하는데 간결한 람다를 적극 활용하자
람다의 this는 바깥 인스턴스를 가리키며, 자신의 참조가 불가능하다.
람다는 이름이 없어 문서화가 불가능하므로, 3줄 이상 줄이 많아지면 쓰지말자.
익명 클래스
익명 클래스는 타입의 인스턴스를 만들 때만 사용하자
람다는 자신을 참조할 수 없으니, 객체가 자신을 참조해야하면 익명클래스를 사용하자.

메서드 참조를 적극적으로 쓰자

NOTE
람다의 가장 큰 장점은 간결함이며, 메서드 참조는 이를 더 극적으로 끌어올릴 수 있다!
// 람다 Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length())); // 메서드 참조 Collections.sort(words, Comparator.comparingInt(String::length));
Java
복사
코드가 훨씬 간결해진다.
메서드 참조를 써서 무조건적으로 좋은건 아니다. 만약 매개변수가 있어 더 보기 좋은경우에는 람다를 사용하자.
즉 메서드 참조를 사용할 때 코드가 더 명확하고 짧아진다면, 메서드 참조를 사용하고 아니라면 람다식을 사용하자.
// 1. 정적 메서드 예제 Function<String, Integer> methodRef = Integer::parseInt; Function<String, Integer> lambda = str -> Integer.parseInt(str); System.out.println(methodRef.apply("100")); System.out.println(lambda.apply("100")); // 2. 바운드 인스턴스 예제 (사용될 객체가 정해져있음 - list) List<String> list = Arrays.asList("Java", "Kotlin", "Scala"); Predicate<String> methodRef = list::contains; Predicate<String> lambda = s -> list.contains(s); System.out.println("List contains 'Java': " + methodRef.test("Java")); System.out.println("List contains 'Python': " + lambda.test("Python")); // 3. 언바운드 인스턴스 예제 (사용될 객체가 정해져 있지 않음 - 직접 문자열 할당) Function<String, String> methodRef = String::toLowerCase; Function<String, String> lambda = str -> str.toLowerCase(); System.out.println(methodRef.apply("HELLO")); System.out.println(lambda.apply("HELLO")); // 4. 클래스 생성자 예제 Supplier<TreeMap<String, Integer>> methodRef = TreeMap<String, Integer>::new; Supplier<TreeMap<String, Integer>> lambda = () -> new TreeMap<String, Integer>(); System.out.println(methodRef.get()); System.out.println(lambda.get()); // 5. 배열 생성자 Function<Integer, int[]> methodRef = int[]::new; Function<Integer, int[]> lambda = len -> new int[len]; System.out.println(Arrays.toString(methodRef.apply(5))); System.out.println(Arrays.toString(lambda.apply(5)));
Java
복사
메서드 참조의 종류

표준 함수형 인터페이스

NOTE
java.util.function에 정의되어 있으며 람다 표현식을 지원하기 위해 등장했습니다!
표준 함수형 인터페이스는 크게 다음과 같은 범주로 나눌 수 있습니다.
// 1. Consumer<T>: void accept(T t) // 출력 Consumer<String> printer = System.out::println; printer.accept("Hello, Consumer!"); // 2. Supplier<T>: T get() // 시간 반환 Supplier<LocalDate> todaySupplier = LocalDate::now; System.out.println("Today is: " + todaySupplier.get()); // 3. Function<T, R>: R apply(T t) // 문자열 길이 반환함수 Function<String, Integer> lengthFunction = String::length; System.out.println("String length is: " + lengthFunction.apply("Hello, Function!")); // 4. Predicate<T>: boolean test(T t) // 검증 Predicate<String> isNonEmpty = s -> !s.isEmpty(); System.out.println("Is non-empty: " + isNonEmpty.test("Hello, Predicate!")); System.out.println("Is non-empty: " + isNonEmpty.test("")); // 5. UnaryOperator(T apply(T t)), BinaryOperator(T apply(T t1, T t2)) UnaryOperator<Integer> squareOperator = x -> x * x; System.out.println("Square of 5 is: " + squareOperator.apply(5)); BinaryOperator<Integer> sumOperator = Integer::sum; System.out.println("Sum of 5 and 7 is: " + sumOperator.apply(5, 7));
Java
복사
이외에도 종류가 많다.

표준 함수형 인터페이스가 아니라 직접 작성하는게 좋은 경우

1.
자주 쓰이며, 이름 자체가 용도를 명확히 설명해주는 경우
2.
반드시 따라야하는 규약이 있는 경우
3.
유용한 디폴트 메서드를 제공해 줄 수 있는 경우
직접 만든 함수형 인터페이스에는 항상 @FunctionallInterface 애너테이션을 사용하라.
1.
해당 클래스의 코드나 설명 문서에서 해당 인터페이스가 람다용으로 설계된것임을 알려준다.
2.
해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
3.
유지보수 과정에서 누가 메서드를 추가하지 못하게 해준다.

함수형 인터페이스 주의점(61 참조)

함수형 인터페이스에서도 오토박싱과 언박싱이 이뤄집니다. 때문에 리턴 타입이 기본 타입이 아니라 기본으로 박싱된 객체인 경우 성능 차이가 있기에 조심해서 사용해야 한다.

스트림

NOTE
스트림은 컬렉션, 배열 등의 데이터를 함수형 스타일로 처리할 수 있게 해주는 기능입니다!
스트림 API의 추상 개념 핵심은 2가지입니다.
1.
스트림은 데이터 원소의 유한/무한 시퀀스를 의미합니다.
2.
스트림 파이프라인은 이 원소들로 수행하는 연산 단계를 표현하는 개념입니다.
스트림 원소들은 어디로부터든 올 수 있으며, 대표적으로 컬렉션, 배열, 파일등 여러 스트림이 존재하며, 기본 타입 값으로는 Int, long, double 이렇게 3가지를 지원합니다.
Object[] objects = Stream.of(1, 2, 3, 4, 5).toArray(); int[] nums = IntStream.of(1, 2, 3, 4, 5).toArray(); long[] longs = LongStream.of(1, 2, 3, 4, 5).toArray(); double[] doubles = DoubleStream.of(1, 2, 3, 4, 5).toArray();
Java
복사
Stream 타입종류
stream: 객체 참조에 대한 Stream
intStream: int 타입에 대한 Stream
longStream: Long 타입에 대한 Stream
doubleStream: Double 타입에 대한 Stream
Stream<Integer> streamOfNumbers = Stream.of(1, 2, 3, 4, 5); Stream<Double> randomNumbers = Stream.generate(Math::random).limit(5); Stream<String> emptyStream = Stream.empty(); IntStream range = IntStream.range(1, 5);
Java
복사
Stream 생성 메서드
of: 일반적인 생성 방법
generate: 무한 스트림을 생성하므로, limit와 조합해서 사용한다.
empty: 빈 스트림을 생성한다.

중간 연산, 종단 연산

NOTE
스트림 파이프라인은 소스 스트림에서 시작해 종단 연산으로 끝나며, 그 사이에 하나 이상의 중간 연산이 있을 수 있습니다!
List<String> words = Arrays.asList("Java", "Stream", "Filter", "Map", "Collect"); // 중간 연산: filter와 map // 종단 연산: collect List<String> filteredWords = words.stream() // Stream<String> 생성 .filter(s -> s.startsWith("J")) // "J"로 시작하는 단어 필터링 .map(String::toUpperCase) // 모든 문자를 대문자로 변환 .collect(Collectors.toList()); // 결과를 List로 수집 System.out.println(filteredWords); // 출력: [JAVA] // 종단 연산: forEach filteredWords.forEach(System.out::println); // JAVA를 출력
Java
복사
중간 연산은 스트림을 변환합니다.
변환된 스트림의 원소 타입은 이전과 같을수도 다를수도 있습니다.
ex) filter, map, sort, limit
종단 연산은 마지막 중간 연산이 내놓은 스트림에 최후의 연산을 가합니다.
원소를 정렬해 컬렉션에 담거나, 특정 원소 하나를 선택하거나 모두 출력하는 식입니다.
ex) collect, forEach, count
 스트림에 대한 연산은 종단 연산 끝에 일어나기 때문에 항상 종단 연산으로 끝을 내줘야한다!

리팩토링 입장의 스트림

NOTE
기존 반복문을 사용하는 코드는 최대한 스트림을 사용하도록 리팩토링하되, 새 코드가 더 나아 보일 때만 반영하자.
이름이 4글자인 사람들만 필터링하고 대문자로 변환하는 로직으로 Stream과 반복문을 비교한다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave"); // stream List<String> filteredNames = names.stream() .filter(name -> name.length() == 4) .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println(filteredNames); // [DAVE]
Java
복사
stream
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave"); List<String> filteredNames = new ArrayList<>(); // 반복문 for (String name : names) { if (name.length() == 4) { filteredNames.add(name.toUpperCase()); } } System.out.println(filteredNames); // [DAVE]
Java
복사
반복문
stream이 그래도 더 보기 좋은거 같다. (실제 상황에서도 비교하고 선택할것)

함수 블록에서는 할 수 없지만 코드 블록으로 할 수 있는 일

코드 블록에서는 범위안의 지역변수를 읽고, 수정할 수 있지만 람다에서는 final이거나 사실상 final 변수만 가능하며 지역 변수를 수정할 수 없다.
코드 블록에서는 return, break, contiune으로 반복문을 제어할 수 있지만, 람다는 불가능하다.

스트림이 알맞는 상황

원소들의 시퀀스를 일관되게 반환한다.
원소들의 시퀀스를 필터링한다.
원소들의 시퀀스를 하나의 연산을 사용해 결합한다. (더하기, 연결하기, 최소값 등 ..)
원소들의 시퀀스를 컬렉션에 모은다
원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.

반환 타입으로 스트림 보다는 컬렉션

NOTE
stream을 올바르게 사용하기 위해서는 Collector을 잘알아야 하며 toList, toSet, toMap, groupingBy, joining에 대해서 잘 알아두자!

toList

List<String> topTwo = freq.keySet().stream() .sorted(Comparator.comparing(freq::get).reversed()) .limit(2) .toList();
Java
복사
toList는 collect를 안쓰고 기본으로 지원해준다.

toMap

// 1,2(key, value) List<Member> people = Arrays.asList( new Member("John", 30), new Member("Sara", 25)); Map<String, Integer> nameToAgeMap = people.stream() .collect(Collectors.toMap(Member::getName, Member::getAge)); // 3번째 값은 충돌 로직 List<Member> people2 = Arrays.asList( new Member("John", 30), new Member("Sara", 25), new Member("John", 22) // 동일 이름, 다른 나이 ); Map<String, Integer> nameToAgeMap2 = people2.stream() .collect(Collectors.toMap( Member::getName, Member::getAge, BinaryOperator.maxBy(Comparator.naturalOrder()))); // 충돌 시 나이가 더 적은 것을 선택
Java
복사

groupingBy

List<Member> members = Arrays.asList( new Member("John", 20), new Member("Sara", 30), new Member("Paul", 20), new Member("Emma", 30) ); // 나이로 묶음 Map<Integer, List<Member>> memberByAge = members.stream() .collect(Collectors.groupingBy(Member::getAge)); System.out.println(memberByAge); // 그룹의 통계(평균이나 묶음 연산을 지원) Map<Integer, Double> collect = members.stream() .collect(Collectors.groupingBy(Member::getAge, Collectors.averagingInt(Member::getAge)));
Java
복사
Collections에는 averagingInt와 같은 속성의 메서드를 지원한다.

스트림 병렬화는 조심해서 써라

NOTE
Stream을 활용해 병렬 처리 기능을 제공하지만, 모든 상황에서 성능 향상을 보장하지 않으며 오히려 성능 저하를 초래할 수 있다!
병렬 스트림은 내부적으로 ForkJoinPool을 사용하여 작업을 여러 스레드에 분배합니다. 이를 통해서 데이터 처리 작업을 병렬로 수행하여 처리 속도를 향상 시킵니다.
병렬 스트림을 사용할 때는 연산이 스레드 안전한지,사용되는 외부 변수가 스레드에 안전한지 주의해야 한다. 상태를 변경하는 연산이나 외부 상태에 의존하는 경우 문제를 일으 킬 수 있다.
스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스이거나 배열 int,long 범위일때 가장 병렬화의 효과가 크다.
int[] numbers = IntStream.range(0, 1000000000).toArray(); long startTime = System.currentTimeMillis(); long count = Arrays.stream(numbers). parallel() .filter(n -> n % 2 == 0) .count(); long endTime = System.currentTimeMillis(); System.out.println("Even numbers count: " + count); System.out.println("Time taken: " + (endTime - startTime) + " ms");
Java
복사
예제코드(parallel이 있는것과 없는것의 시간차이가 크다.)
이 자료구조들은 모두 데이터를 원하는 크기로 나눌 수 있어 다수의 스레드에 분배하기 좋다.
나누는 작업은 Spliterator가 담당하며, Spliterator 객체는 Stream이나 Iterable의 spliterator메서드로 얻어올 수 있다.
해당 자료구조들은 원소들을 순차적으로 실행할때 참조 지역성이 뛰어나다는 것이다. 참조 지역성이 낮으면 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 멍하게 보내게된다.
 스트림 병렬화는 오직 성능 최적화 수단이며, 병렬화 사용가치가 있는 경우에만 사용한다.