관리 메뉴

개발그래머

[자바스터디 번외] 문자열, 콜렉션, 스트림 본문

Java

[자바스터디 번외] 문자열, 콜렉션, 스트림

임요환 2023. 9. 24. 16:13

목표

  • 문자열, 콜렉션, 스트림에 대해 학습하세요

학습할 것 (필수)

  • 문자열(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