티스토리 뷰
Iteration
- 특징
- 컬렉션의 요소를 순회하는 과정
- 명령형(Imperative) 프로그래밍 스타일로, 상태를 명시적으로 관리
- 데이터 처리 로직을 직접 작성하며 가독성이 떨어질 수 있음
- 코드 실행 순서를 한눈에 알 수 있음
- 장점
- 직관적이고 디버깅이 쉬움
- 간단한 작업에는 Stream API보다 효율적일 수 있음
- 상태를 직접 제어할 수 있어 더 세밀한 제어 가능
- 단점
- 반복문이 복잡해질수록 코드의 유지보수가 어려움
- 병렬 처리를 지원하지 않음
전통적 for 루프
- 사용 사례: 인덱스를 통해 요소에 접근하거나 요소를 수정해야 할 때.
- 예제:
List<String> list = Arrays.asList("A", "B", "C");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
향상된 for 루프
- 사용 사례
- 컬렉션의 요소를 읽기만 할 때
- 예제
for (String element : list) {
System.out.println(element);
}
Iterator
- 사용 사례
- 순회 중 컬렉션을 안전하게 수정할 때
- 예제
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
- 중요 질문: Iterator와 ListIterator의 차이점은?
- 답변: Iterator는 단방향 순회만 가능하며, ListIterator는 양방향 순회를 지원하며 순회 중 요소를 추가할 수도 있습니다.
ForEach 메서드(람다 표현식)
- 사용 사례
- 간결한 문법으로 컬렉션을 순회할 때
- 예제
list.forEach(element -> System.out.println(element));
ListIterator (리스트 양방향 순회용)
- 사용 사례
- 양방향 순회가 필요한 리스트 작업 시
- 예제
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.println(listIterator.next());
}
심화 개념
- Fail-Fast vs. Fail-Safe Iterators
- Fail-Fast
- 컬렉션이 순회 중 구조적으로 변경되면 ConcurrentModificationException이 발생
- 예) ArrayList, HashMap
- Fail-Safe
- 컬렉션의 복사본에서 작업하므로 예외가 발생하지 않음. 하지만 실시간 변경 사항은 반영되지 않음
- 예) ConcurrentHashMap, CopyOnWriteArrayList
- Fail-Fast
면접 대비 질문
- Iterator와 향상된 for 루프의 차이점은?
- 답변: 향상된 for 루프는 간단하고 읽기 쉬운 코드 작성을 지원하지만, 순회 중 요소를 제거할 수 없습니다. 반면, Iterator는 remove() 메서드를 통해 안전하게 컬렉션을 수정할 수 있습니다.
- forEach 루프 중 컬렉션을 수정할 수 있나요? 그렇지 않다면 왜 그런가요?
- 답변: 수정할 수 없습니다. ConcurrentModificationException이 발생합니다. 안전하게 수정하려면 Iterator를 사용해야 합니다.
스트림 API
- 특징
- Java 8에서 도입된 Stream API는 컬렉션과 데이터 시퀀스를 처리하기 위한 강력한 도구
- 선언형(Declarative) 프로그래밍 스타일로, 데이터 처리 파이프라인을 정의
- 중간 연산(Lazy Evaluation)과 최종 연산(Eager Evaluation)을 구분
- 불변 객체를 활용하여 상태를 안전하게 관리
- 장점
- 가독성이 뛰어나며, 코드가 간결
- 병렬 처리(Parallel Stream)를 지원하여 대량 데이터 처리에 적합
- 데이터 필터링, 매핑, 정렬 등의 작업을 직관적으로 구현 가능
- 단점
- 작은 데이터 처리 작업에서 성능이 떨어질 수 있음
- 병렬 스트림 사용 시 쓰레드 동기화 문제에 주의 필요
- 디버깅이 어려움(람다 표현식의 특성상)
스트림 API의 주요 기능
- 선언적 코드
- 무엇을 해야 하는지를 표현, 어떻게 할지를 작성하지 않아도 됨
- Chained Operations
- 연산을 체이닝하여 파이프라인 구성 가능
- Lazy Evaluation
- 터미널 연산이 호출될 때만 실행.
예제
필터링과 매핑
List<String> names = Arrays.asList("John", "Jane", "Jake", "Jill");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("J"))
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(filteredNames); // 출력: [JOHN, JANE, JAKE, JILL]
짝수의 합 계산
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.reduce(0, Integer::sum);
System.out.println(sum); // 출력: 12
리스트 정렬
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
List<String> sortedFruits = fruits.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedFruits); // 출력: [Apple, Banana, Cherry, Date]
심화 개념
- 분할(partitioning)
- Collectors.partitioningBy() 메서드를 사용하여 조건에 따라 데이터를 두 그룹으로 분할할 수 있습니다
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> partitionedByEven = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println(partitionedByEven);
// 출력: {false=[1, 3, 5], true=[2, 4, 6]}
- 그룹화(grouping)
- Collectors.groupingBy() 메서드를 사용하여 특정 기준에 따라 데이터를 그룹화할 수 있습니다
List<String> items = Arrays.asList("apple", "banana", "cherry", "apricot", "blueberry");
Map<Character, List<String>> groupedByFirstLetter = items.stream()
.collect(Collectors.groupingBy(item -> item.charAt(0)));
System.out.println(groupedByFirstLetter);
// 출력: {a=[apple, apricot], b=[banana, blueberry], c=[cherry]}
- 병렬 스트림
- 사용 시기
- 대규모 데이터 세트 및 CPU 집약적 작업.
- parallelStream() 메서드를 사용하여 병렬 처리를 수행할 수 있습니다.
- 주의사항
- 병렬 스트림은 모든 상황에서 성능 향상을 보장하지 않으며, 오히려 오버헤드가 발생할 수 있음
- 사용 전에 성능 테스트를 수행하는 것이 좋습니다
- 사용 시기
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sumOfSquares = numbers.parallelStream()
.map(n -> n * n)
.reduce(0, Integer::sum);
System.out.println(sumOfSquares); // 출력: 91
- 리듀싱 (Reducing)
- reduce() 메서드를 사용하여 스트림의 요소를 하나의 값으로 합칠 수 있음
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println(sum); // 출력: 15
- 요약(Summarizing)
- Collectors.summarizingInt() 등을 사용하여 숫자 데이터의 통계를 요약할 수 있음
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats);
// 출력: IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}
- 커스텀 컬렉
- Collector 인터페이스를 구현하여 사용자 정의 컬렉터를 만들 수 있음
Collector<String, StringJoiner, String> customCollector = Collector.of(
() -> new StringJoiner(", "), // supplier
StringJoiner::add, // accumulator
StringJoiner::merge, // combiner
StringJoiner::toString // finisher
);
List<String> items = Arrays.asList("apple", "banana", "cherry");
String result = items.stream()
.collect(customCollector);
System.out.println(result); // 출력: apple, banana, cherry
- 무한 스트림 생성
- Stream.iterate() 또는 Stream.generate() 메서드를 사용하여 무한 스트림을 생성할 수 있음
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2);
- 무한 스트림 제한
- limit() 메서드를 사용하여 스트림의 크기를 제한할 수 있음
List<Integer> limitedNumbers = infiniteStream
.limit(5)
.collect(Collectors.toList());
System.out.println(limitedNumbers); // 출력: [0, 2, 4, 6, 8]
- 스트림 연
- Stream.concat() 메서드를 사용하여 두 스트림을 연결할 수 있음
Stream<String> stream1 = Stream.of("apple", "banana");
Stream<String> stream2 = Stream.of("cherry", "date");
Stream<String> combinedStream = Stream.concat(stream1, stream2);
combinedStream.forEach(System.out::println);
// 출력: apple, banana, cherry, date
면접 대비 질문
- Stream API와 전통적 반복(iteration)의 차이점은?
- 답변: Stream API는 선언적 접근 방식을 사용하며, 전통적 반복은 명령형 논리에 집중하여 단계별로 작성합니다
- 병렬 스트림 사용의 장단점은?
- 답변:
- 장점: 병렬 스트림은 여러 스레드를 활용하여 대규모 데이터를 더 빠르게 처리할 수 있습니다
- 단점: 작은 데이터 세트에서는 오버헤드가 발생하고, 스레드 안전 문제를 야기할 수 있습니다
- 답변:
- 스트림의 중간 연산과 터미널 연산의 차이를 설명하세요
- 답변: 중간 연산(e.g., filter, map)은 게으르게 실행되며 스트림을 반환합니다. 터미널 연산(e.g., collect, forEach)은 파이프라인을 실행하고 결과를 반환합니다
Comparable과 Comparator 인터페이스
- Comparable과 Comparator는 Java에서 객체의 정렬 로직을 정의하는 데 사용되는 두 가지 인터페이스
- 둘 다 java.util 패키지에 포함되어 있으며 면접에서 자주 다뤄짐
Comparable 인터페이스
- Comparable 인터페이스는 객체의 자연 순서를 정의하는 데 사용
- 이를 사용하려면 클래스에서 compareTo() 메서드를 구현해야 합니다.
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
}
List<Student> students = Arrays.asList(
new Student("Alice", 23),
new Student("Bob", 21),
new Student("Charlie", 25)
);
Collections.sort(students);
students.forEach(s -> System.out.println(s.name + ", " + s.age));
Comparator 인터페이스
- Comparator 인터페이스는 여러 정렬 기준을 정의할 수 있는 더 큰 유연성을 제공합니다.
List<Student> students = Arrays.asList(
new Student("Alice", 23),
new Student("Bob", 21),
new Student("Charlie", 25)
);
Comparator<Student> nameComparator = Comparator.comparing(student -> student.name);
Comparator<Student> ageComparator = Comparator.comparingInt(student -> student.age);
// 이름 기준 정렬
students.sort(nameComparator);
// 나이 기준 정렬
students.sort(ageComparator);
심화 개념
- Comparator 체이닝
- 여러 기준을 기반으로 객체를 정렬할 때 사용하는 기법
- 하나의 기준으로 정렬한 후, 같은 값이 있는 경우 다음 기준으로 추가 정렬을 수행할 수 있음
- 첫 번째 Comparator: 주된 정렬 기준을 설정합니다
- thenComparing: 첫 번째 기준으로 동일한 값들이 발생하면 두 번째 기준을 적용합니다
예제
import java.time.LocalDate;
import java.util.*;
class Employee {
String name;
String department;
double salary;
LocalDate joiningDate;
public Employee(String name, String department, double salary, LocalDate joiningDate) {
this.name = name;
this.department = department;
this.salary = salary;
this.joiningDate = joiningDate;
}
@Override
public String toString() {
return name + " | " + department + " | " + salary + " | " + joiningDate;
}
}
public class ComplexComparatorChaining {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Engineering", 75000, LocalDate.of(2015, 1, 15)),
new Employee("Bob", "Engineering", 80000, LocalDate.of(2013, 6, 1)),
new Employee("Charlie", "HR", 70000, LocalDate.of(2016, 9, 20)),
new Employee("David", "HR", 70000, LocalDate.of(2015, 3, 10)),
new Employee("Eve", "Engineering", 75000, LocalDate.of(2014, 7, 25))
);
// Comparator 체이닝: 부서 → 연봉(내림차순) → 이름 → 입사 날짜
Comparator<Employee> employeeComparator = Comparator.comparing(Employee::getDepartment)
.thenComparing(Comparator.comparing(Employee::getSalary).reversed())
.thenComparing(Employee::getName)
.thenComparing(Employee::getJoiningDate);
// 정렬 수행
employees.sort(employeeComparator);
// 결과 출력
employees.forEach(System.out::println);
}
}
출력
Bob | Engineering | 80000.0 | 2013-06-01
Eve | Engineering | 75000.0 | 2014-07-25
Alice | Engineering | 75000.0 | 2015-01-15
David | HR | 70000.0 | 2015-03-10
Charlie | HR | 70000.0 | 2016-09-20
Comparable과 Comparator의 차이점
면접 대비 질문
- Comparable과 Comparator의 사용 사례는?
- 답변: 클래스 내부에서 자연 순서를 정의하려면 Comparable을 사용하고, 외부에서 다중 기준으로 정렬하려면 Comparator를 사용합니다
- 컬렉션을 다중 기준으로 정렬하려면 어떻게 하나요?
- 답변: Comparator 체이닝을 사용합니다
- 예) Comparator.comparing(...).thenComparing(...).
- 복잡한 객체에 대해 커스텀 Comparator를 작성할 수 있나요?
- 답변: 가능합니다. 람다 표현식 또는 메서드 참조를 사용하여 정렬 로직에 맞게 작성합니다
질문 예시
더보기
기초 질문
- Iteration과 Stream API의 가장 큰 차이점은 무엇인가요?
답변:
Iteration은 명령형(Imperative) 프로그래밍 스타일로 상태를 명시적으로 제어하며, 데이터를 하나씩 처리하는 방식입니다. Stream API는 선언형(Declarative) 스타일로 데이터 처리 로직을 파이프라인 형태로 정의하며, 가독성이 뛰어나고 병렬 처리를 지원합니다.- Iteration은 코드가 장황해질 수 있지만 직관적이고 디버깅이 쉽습니다.
- Stream API는 더 간결하지만 Lazy Evaluation으로 동작하여 실행 시점까지 연산이 지연됩니다.
- Stream API의 중간 연산(Intermediate Operation)과 최종 연산(Terminal Operation)의 차이는 무엇인가요?
답변:- 중간 연산: 데이터를 변환하거나 필터링하며, Lazy Evaluation으로 실행 시점까지 동작하지 않습니다. 예: filter, map, sorted.
- 최종 연산: Stream의 처리를 종료하고 결과를 반환합니다. 즉, 최종 연산을 호출할 때 비로소 중간 연산이 실행됩니다. 예: collect, forEach, reduce.
- 반복문 대신 Stream API를 사용하는 이유는 무엇인가요?
답변:- 코드의 가독성과 간결성을 높이기 위해 사용합니다.
- 병렬 처리를 쉽게 구현할 수 있어 대량 데이터 처리에서 효율적입니다.
- 데이터를 선언적으로 처리하므로 비즈니스 로직에 집중할 수 있습니다.
실전 질문
- Stream API에서 filter, map, flatMap의 차이를 설명하세요.
답변:- filter: 조건에 맞는 요소를 선택합니다.
List<Integer> even = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());
- map: 각 요소를 변환합니다.
List<Integer> doubled = numbers.stream().map(n -> n * 2).collect(Collectors.toList());
- flatMap: 중첩된 컬렉션을 평탄화하여 하나의 스트림으로 만듭니다.
List<String> words = lists.stream().flatMap(List::stream).collect(Collectors.toList());
- filter: 조건에 맞는 요소를 선택합니다.
- Stream에서 collect(Collectors.toList())를 사용하는 이유와 내부 동작은 무엇인가요?
답변:- Stream의 결과를 List 형태로 수집하기 위해 사용합니다.
- 내부적으로 Collector는 요소를 Supplier가 제공한 빈 컬렉션에 추가하고, 결과를 반환합니다. 이는 병렬 스트림에서도 동작하도록 설계되어 있습니다.
- parallelStream()을 사용하는 경우 언제 성능이 저하될 수 있나요?
답변:- 데이터가 적을 때 오히려 쓰레드 생성 및 관리 오버헤드로 성능이 저하됩니다.
- 요소 간 의존성이 강하거나 공유 리소스를 사용하는 작업은 동기화 문제로 인해 성능이 저하됩니다.
- 순서가 중요한 작업에서는 병렬 처리가 적합하지 않습니다.
- sorted()와 Comparator를 사용하여 정렬하는 코드를 작성해 보세요.
답변:List<String> sortedList = names.stream() .sorted(Comparator.naturalOrder()) // 오름차순 .collect(Collectors.toList()); List<String> reverseSortedList = names.stream() .sorted(Comparator.reverseOrder()) // 내림차순 .collect(Collectors.toList());
심화 질문
- Stream API가 메모리와 CPU를 효율적으로 사용하는 방법을 설명하세요.
답변:- Lazy Evaluation을 통해 필요한 데이터만 처리하여 메모리 낭비를 줄입니다.
- parallelStream은 CPU의 여러 코어를 활용해 작업을 병렬 처리하여 처리 속도를 높입니다.
- 데이터의 연속성을 유지하며 컬렉션과 같은 추가 메모리 할당을 줄입니다.
- Stream API와 forEach는 다르게 동작할 수 있습니다. 그 이유는 무엇인가요?
답변:
Stream API는 선언적 방식으로 데이터를 처리하며, 데이터의 순서와 병렬성을 제어합니다. 반면, forEach는 최종 연산으로 Stream이 아닌 상태에서 반복 동작을 수행하며, 병렬 처리에서 요소 순서를 보장하지 않습니다. 대신 forEachOrdered를 사용하면 순서를 보장할 수 있습니다.
- reduce() 메서드와 그 사용 사례를 설명하세요.
답변:- reduce()는 스트림의 모든 요소를 누적하여 하나의 결과를 반환합니다.
- 예: 숫자의 합계 계산.
int sum = numbers.stream().reduce(0, Integer::sum);
- Java의 Stream API와 다른 언어의 유사한 기능과 비교해보세요.
답변:- Python의 List Comprehension: 선언적 방식으로 데이터를 변환 및 필터링하며, Stream API와 유사한 간결성을 제공합니다. 그러나 병렬 처리 지원이 부족합니다.
- C#의 LINQ: Java의 Stream API와 비슷하지만, SQL과 유사한 구문을 사용하며 더 풍부한 데이터 쿼리 기능을 제공합니다.
코딩 테스트 및 구현 질문
- 리스트에서 짝수만 필터링하고, 값에 2를 곱한 후, 내림차순 정렬하는 코드를 작성하세요.
답변:
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
- Stream API를 이용하여 문자열 리스트에서 중복을 제거하고, 대문자로 변환한 결과를 반환하세요.
답변:
List<String> result = strings.stream()
.distinct()
.map(String::toUpperCase)
.collect(Collectors.toList());
'Language > Java' 카테고리의 다른 글
[JAVA] 람다와 함수형 인터페이스 (2) | 2025.01.13 |
---|---|
[JAVA] 예외 처리에 대하여 (0) | 2025.01.10 |
[JAVA] Collection에 대하여 (0) | 2025.01.09 |
[JAVA] JVM에 대하여 (1) | 2025.01.08 |
[JAVA] Enum에 대하여 (0) | 2025.01.08 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Spring
- 해시 테이블
- 탐색 알고리즘
- 우선순위 큐
- CS
- Java
- CPU 스케줄링
- 분할 정복
- i/o모델
- MSA
- Spring Boot
- 데이터베이스
- 스프링
- 자바
- 자료구조
- restful api
- 백트래킹
- 운영체제
- 그리디 알고리즘
- 우아한 테크코스
- HTTP
- 동적 프로그래밍
- 알고리즘
- db
- devops
- 우테코
- 프리코스
- k8
- B+Tree
- TRIE
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
글 보관함