목표
학습할 것 (필수)
- 문자열(String)
- 콜렉션(Collection)
- 스트림(Stream)
문자열(String)
String의 기본 개념
- Java에서 불변(immutable) 객체임
- 한 번 생성된 String 객체는 변경할 수 없음
- 문자열 조작은 항상 새로운 문자열 객체를 생성함
- java.lang 패키지에 포함되어 있으며 별도의 임포트 없이 사용할 수 있음
String의 불변성
- 멀티스레드 환경에서도 String 객체를 안전하게 공유할 수 있음(Thread-safe)
- JVM은 문자열 리터럴을 String Pool이라는 메모리 영역에 저장하므로 동일한 문자열 리터럴이 재사용되므로 메모리를 절약할 수 있음(캐싱 가능)
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true (String Pool 동일 객체)
String s3 = new String("Hello");
System.out.println(s1 == s3); // false (새로운 객체)
String Pool(Interning)
- 문자열 리터럴은 JVM의 String Pool에 저장됨
- 문자열 리터럴이 새로 생성될 때 JVM은 String Pool에 동일한 값의 문자열이 있는지 확인하며 있다면 기존 객체를 재사용하고 없으면 새로 생성함
- String interning: intern() 메서드는 Heap에 생성된 문자열 객체를 String Pool에 추가하고 String Pool의 참조를 반환함
String s1 = new String("Hello");
String s2 = s1.intern(); // s2는 String Pool의 "Hello" 참조
System.out.println(s1 == s2); // false
String 클래스 메서드
메서드 |
설명 |
length() |
문자열의 길이를 반환합니다 |
charAt(int index) |
지정된 인덱스의 문자를 반환합니다 |
substring(int start, int end) |
특정 범위의 하위 문자열을 반환합니다 |
toLowerCase() |
문자열을 모두 소문자로 변환합니다 |
toUpperCase() |
문자열을 모두 대문자로 변환합니다 |
trim() |
문자열의 앞뒤 공백을 제거합니다 |
replace(char old, char new) |
특정 문자를 다른 문자로 대체합니다 |
split(String regex) |
주어진 정규 표현식을 기준으로 문자열을 나눕니다 |
contains(CharSequence seq) |
문자열에 특정 문자가 포함되어 있는지 확인합니다 |
equals(Object obj) |
두 문자열의 값을 비교합니다 |
compareTo(String anotherString) |
사전순으로 문자열을 비교합니다 |
StringBuilder와 StringBuffer
- String은 불변이기 때문에 문자열을 조작할 때마다 새로운 객체가 생성되므로 대량의 문자열 조작이 필요한 경우 StringBuilder 또는 StringBuffer를 사용하면 더 효율적임
- 변경 가능한 문자열 객체
- StringBuilder: 멀티스레드 환경에서는 안전하지 않지만 단일 스레드에서는 빠름
- StringBuffer: 멀티스레드 환경에서 안전하도록 동기화(synchronzied)됨
- 문자열 연결을 +로 할 때 내부적으로 컴파일러가 StringBuilder를 사용하도록 최적화함
String result = "Hello" + "World";
// 컴파일 후
String result = new StringBuilder("Hello").append("World").toString();
CharSequence
- String 클래스는 CharSequence 인터페이스를 구현한 하나의 구체적 클래스임
- CharSequence는 문자열 관련 작업을 일반화하기 위한 인터페이스로 String, StringBuilder, StringBuffer 등이 구현체임
public void printMessage(CharSequence message) {
System.out.println(message);
}
printMessage("String"); // String
printMessage(new StringBuilder("StringBuilder")); // StringBuilder
콜렉션(Collection)
- Collection Framework는 데이터의 집합을 효율적으로 관리하고 조작하기 위한 표준화된 클래스와 인터페이스 모음임
- 데이터를 저장, 검색, 정렬, 조작하는 다양한 구조와 알고리즘을 제공함
주요 인터페이스와 클래스
Collection 인터페이스
- 모든 컬렉션 클래스의 루트 인터페이스
- 데이터를 그룹으로 관리하는데 사용됨
List
- 순서가 있는 컬렉션으로 중복된 요소를 허용함
- 각 요소는 인덱스를 통해 접근 가능
구현체 |
설명 |
ArrayList |
동적 배열. 검색 속도가 빠르지만, 삽입/삭제가 느립니다 |
LinkedList |
이중 연결 리스트. 삽입/삭제가 빠르지만, 검색 속도가 느립니다 |
Vector |
ArrayList와 유사하나, 동기화(synchronized)되어 멀티스레드 환경에서 안전합니다 |
Stack |
Vector를 상속받은 LIFO(Last-In-First-Out) 구조입니다 |
메서드 |
설명 |
add(E element) |
요소 추가 |
get(int index) |
특정 위치의 요소 가져오기 |
remove(int index) |
특정 위치의 요소 제거 |
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
System.out.println(list.get(1)); // "Python"
Set
- 중복을 허용하지 않는 컬렉션
- 요소의 순서를 보장하지 않거나 특정 기준으로 정렬됨
구현체 |
설명 |
HashSet |
순서를 보장하지 않고 빠른 검색과 삽입/삭제가 가능합니다 |
LinkedHashSet |
삽입된 순서를 유지합니다 |
TreeSet |
요소를 정렬된 상태로 유지하며, NavigableSet 인터페이스를 구현합니다 |
메서드 |
설명 |
add(E element) |
요소 추가 |
contains(Object obj) |
특정 요소 포함 여부 확인 |
remove(Object obj) |
특정 요소 제거 |
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Python");
set.add("Java"); // 중복된 요소는 추가되지 않음
System.out.println(set); // [Java, Python]
Queue
- FIFO 구조의 컬렉션
- PriorityQueue는 기본 구현체로 요소를 우선순위에 따라 정렬함
구현체 |
설명 |
PriorityQueue |
우선순위에 따라 요소를 정렬하며, null 값을 허용하지 않습니다 |
LinkedList |
Queue 인터페이스도 구현하며, FIFO 및 LIFO 모두 지원할 수 있습니다 |
메서드 |
설명 |
add(E element), offer(E element) |
요소 추가 |
remove(), poll() |
요소 제거 |
peek() |
첫 번째 요소 조회 (제거하지 않음) |
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.add(2);
queue.add(3);
System.out.println(queue.poll()); // 1 (첫 번째 요소 제거)
System.out.println(queue.peek()); // 2 (두 번째 요소 조회)
Map
- 키, 값 쌍으로 구성된 컬렉션
- 키는 중복될 수 없으며, 값은 중복을 허용함
구현체 |
설명 |
HashMap |
순서를 보장하지 않으며, 빠른 검색과 삽입/삭제를 제공합니다 |
LinkedHashMap |
삽입된 순서를 유지합니다 |
TreeMap |
키를 정렬된 순서로 유지하며, NavigableMap 인터페이스를 구현합니다 |
Hashtable |
동기화된 Map으로, 멀티스레드 환경에서 안전합니다 |
ConcurrentHashMap |
동기화를 제공하며, 높은 성능의 멀티스레드 환경에 적합합니다 |
- 주요 메서드 |
|
메서드 |
설명 |
put(K key, V value) |
키-값 쌍 추가 |
get(Object key) |
키에 해당하는 값 반환 |
remove(Object key) |
키-값 쌍 제거 |
containsKey(Object key) |
특정 키 포함 여부 확인 |
containsValue(Object value) |
특정 값 포함 여부 확인 |
Map<String, Integer> map = new HashMap<>();
map.put("Java", 90);
map.put("Python", 80);
System.out.println(map.get("Java")); // 90
System.out.println(map.containsKey("Python")); // true
컬렉션 정렬
메서드 |
설명 |
sort(List list) |
기본 정렬 |
sort(List list, Comparator c) |
사용자 정의 정렬 |
List<Integer> numbers = Arrays.asList(5, 1, 4, 3, 2);
Collections.sort(numbers);
System.out.println(numbers); // [1, 2, 3, 4, 5]
컬렉션의 동기화
- 기본적으로 동기화되지 않음
- 동기화된 컬렉션이 필요하면 Collections.synchronizedXXX() 메서드를 사용할 수 있음
List<String> list = Collections.synchronizedList(new ArrayList<>());
Set<String> set = Collections.synchronizedSet(new HashSet<>());
- 위와 같이 동기화하는 방식보다는 java.util.concurrent 패키지에서 제공하는 동시성 컬렉션을 사용하는 것이 성능적으로 좋음
클래스 |
설명 |
ConcurrentHashMap |
고성능 동기화된 Map. 세그먼트 잠금(lock segmentation) 기술로 성능을 최적화 |
CopyOnWriteArrayList |
읽기 작업이 많은 환경에서 적합. 쓰기 작업 시 내부 배열 복사 후 변경 |
CopyOnWriteArraySet |
CopyOnWriteArrayList를 기반으로 한 동기화된 Set |
ConcurrentLinkedQueue |
동기화된 FIFO(First-In-First-Out) Queue |
ConcurrentSkipListMap |
정렬된 키를 유지하며 동기화된 Map |
ConcurrentSkipListSet |
정렬된 키를 유지하며 동기화된 Set |
Stream API와 컬렉션
- Stream API를 사용하면 컬렉션 데이터를 효율적으로 처리할 수 있음
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println); // ALICE
스트림(Stream)
- Java 8에서 도입된 기능으로 데이터를 선언적이고 함수형으로 처리할 수 있게 해줌
- 데이터를 필터링, 변환, 수집 등의 작업을 수행할 수 있으며 병렬 처리를 지원해 성능 향상을 도모할 수 있음
Stream이란?
- Stream은 데이터의 흐름을 추상화한 것으로 데이터의 저장소가 아니라 데이터를 처리하는 기능을 제공함
- Stream은 원본 데이터를 변경하지 않고 데이터를 읽고 조작하는 일련의 연산을 제공함
- 스트림 파이프라인은 보통 3단계로 구성됨
- 생성: 데이터를 스트림으로 변환
- 중간 연산: 데이터를 변환, 필터링, 정렬 등
- 최종 연산: 데이터를 결과로 반환
주요 특징
- 선언적 스타일: 데이터 처리 로직을 함수형으로 표현
- 중간 연산과 최종 연산
- 중간 연산은 스트림을 반환하며 게으르게(lazy) 실행
- 최종 연산은 스트림의 작업을 수행하고 결과를 반환
- 데이터 변경 없음: Stream은 원본 데이터를 변경하지 않음
- 병렬 처리 지원: 병렬 스트림으로 대규모 데이터 처리 가능
주요 구성
Stream 생성
// 컬렉션에서 생성
List<String> list = Arrays.asList("Java", "Python", "C++");
Stream<String> stream = list.stream();
// 배열에서 생성
String[] array = {"Java", "Python", "C++"};
Stream<String> stream = Arrays.stream(array);
// 값에서 생성
Stream<String> stream = Stream.of("Java", "Python", "C++");
// 파일에서 생성
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.forEach(System.out::println);
}
중간 연산
- lazy하게 실행되며, 스트림을 반환하며 이를 연결하여 파이프라인을 구성함
메서드 |
설명 |
예제 |
filter |
조건에 맞는 요소만 포함 |
filter(x -> x.startsWith("A")) |
map |
각 요소를 변환 |
map(String::toUpperCase) |
flatMap |
중첩된 스트림을 평평하게 변환 |
flatMap(Collection::stream) |
sorted |
요소를 정렬 |
sorted() 또는 sorted(Comparator.naturalOrder()) |
distinct |
중복된 요소 제거 |
distinct() |
limit |
처음 N개 요소 제한 |
limit(3) |
skip |
처음 N개 요소 건너뜀 |
skip(2) |
List<String> list = Arrays.asList("Java", "Python", "C++", "Java");
list.stream()
.filter(name -> name.startsWith("J")) // "Java", "Java"
.distinct() // "Java"
.map(String::toUpperCase) // "JAVA"
.forEach(System.out::println); // JAVA
최종 연산
- 스트림의 작업을 실행하고 결과를 반환하거나 처리함
메서드 |
설명 |
반환 타입 |
forEach |
각 요소에 대해 특정 작업 수행 |
void |
collect |
결과를 컬렉션 또는 다른 형식으로 수집 |
Collector |
toArray |
요소를 배열로 반환 |
배열 |
reduce |
요소를 하나로 축소 |
Optional 또는 사용자 정의 |
count |
요소 개수 반환 |
long |
findFirst |
첫 번째 요소 반환 |
Optional |
findAny |
아무 요소 반환 (병렬 처리 시 유용) |
Optional |
allMatch |
모든 요소가 조건을 만족하는지 확인 |
boolean |
anyMatch |
하나의 요소라도 조건을 만족하는지 확인 |
boolean |
noneMatch |
모든 요소가 조건을 만족하지 않는지 확인 |
boolean |
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum); // 합계: 15
System.out.println(sum);
List<String> collected = numbers.stream()
.map(String::valueOf) // 변환
.collect(Collectors.toList());
System.out.println(collected); // ["1", "2", "3", "4", "5"]
병렬 스트림
- 멀티스레드를 사용하여 작업을 병렬로 처리함
- 데이터가 크고 연산이 많을 때 유리함
List<String> list = Arrays.asList("A", "B", "C");
list.parallelStream()
.forEach(System.out::println); // 출력 순서가 보장되지 않음
- 공유 리소스에 접근하는 경우 동기화를 신경 써야 함
- 작업량이 적을 경우 병렬 처리가 오히려 성능을 저하시킬 수 있음
Collector와 Collectors
- Collector는 스트림의 결과를 수집하는 데 사용됨, 데이터를 수집하는 데 필요한 방법을 정의한 인터페이스
- Collectors는 Collector를 생성하기 위한 유틸리티 클래스임, 이미 구현된 다양한 Collector 메서드들을 제공함
Collector 메서드 |
설명 |
toList() |
리스트로 수집 |
toSet() |
집합으로 수집 |
toMap(keyMapper, valueMapper) |
맵으로 수집 |
joining(delimiter) |
문자열로 연결 |
groupingBy(classifier) |
특정 기준으로 그룹화 |
partitioningBy(predicate) |
조건에 따라 분할 |
List<String> names = Arrays.asList("Apple", "Banana", "Cherry", "Apple");
// 리스트로 수집
List<String> list = names.stream()
.distinct()
.collect(Collectors.toList());
// 문자열로 연결
String joined = names.stream()
.collect(Collectors.joining(", ")); // "Apple, Banana, Cherry, Apple"
// 그룹화
Map<Integer, List<String>> grouped = names.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(grouped); // {5=[Apple, Apple], 6=[Banana], 6=[Cherry]}
Stream과 기본형 스트림
- IntStream, LongStream, DoubleStream 등의 기본 스트림은 성능 최적화를 위해 사용됨
IntStream.range(1, 5) // 1부터 5까지
.forEach(System.out::println); // 출력: 1, 2, 3, 4