-
Java) String 부수기CS 2024. 8. 6. 23:11
개요
String 은 수정이 가능한가요?
어떻게 불변을 만드는걸까요?
왜 불변을 만드는걸까요?기존에 String 은 단순히 문자열 관련된것을 쓸때만 사용했는데, 항상 느끼는거지만 String 에 관한 질문은 무조건 나온다.
그것도 계속된 질문으로 A를 알아? 그럼 B는 알까? 그러면 C는 알까? 하는 수준으로 진짜 어디까지 알아야 하나 싶을정도로 파도파도 계속 더 알아야한다.
한번이라도 모르면 끝이다. 그렇기에 이번 글을 통해 진짜 심해 깊은곳까지 탐험할 예정이다. 계속된 수정과 끝이없는 업데이트를 통해...
String 이란?
String 클래스는 문자열을 나타내는 데 사용되는 불변(immutable) 객체
이 한마디에 굉장히 많은 말이 함축되어있다, String 은 클래스이고, 문자"열"이며, "불변" "객체"이다.
우선 String "클래스" 에 대해 알아보자.
String 클래스의 구조
public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc
- final class String
- final 로 선언된 이유는 주로 보안, 성능, 메모리 효율성, 안정성 등을 보장하기 위해서라는데, String 의 경우 단순히 String 만 사용하는게 아니라 파생되는 List,Map 같은곳에서도 많이 활용되기에 불변클래스로 하였고,
- final 키워드를 통해 상속을 방지하고, 객체의 불변성을 유지함으로써 Java의 핵심 기능과 데이터 구조가 일관되고 안전하게 동작할 수 있다고한다.
- Serializable 인터페이스를 구현하여 문자열 객체를 직렬화할 수 있다.
- 직렬화 : 자바 직렬화는 객체를 바이트 스트림으로 변환하여 저장하거나 전송할 수 있게 하는 과정
- 직렬화 : 자바 직렬화는 객체를 바이트 스트림으로 변환하여 저장하거나 전송할 수 있게 하는 과정
- Comparable<String> 인터페이스를 구현하여 문자열 객체를 비교할 수 있습니다.
- Comparable 인터페이스를 구현하고 있어, 문자열을 정렬하거나 비교할 때 compareTo 메서드를 사용할 수 있다.
String str1 = "apple"; String str2 = "banana"; int result = str1.compareTo(str2);
- CharSequence 인터페이스를 구현하여 문자열을 문자 시퀀스로 다룰 수 있습니다. ex. charAt,length() 등등...
필드
private final char value[]; private final int hash; // 캐시된 해시값
- value 배열은 char 타입의 배열로, 실제 문자열 데이터를 저장
- value 배열은 final로 선언되어 있으며, 이를 통해 문자열의 불변성을 유지
- hash 필드는 문자열의 해시 코드를 캐시하여 해시 기반 컬렉션에서의 성능을 향상시킵니다.
그러면 여기서 class 도 final 이고 내부 변수도 final 인데 String 을 선언한건 계속 연산이 가능하잖아? 라고 생각할 수 있습니다.
사실 String 도 객체라고 생각하시면 편합니다.
여기 Person 객체로 예시를 들어보겠습니다.
public final class Person { //class 를 final 로 선언 private final String name; //내부 변수도 final 로 선언 public Person(String name) { this.name = name; } public String getName() { return name; } }
이 객체를 생성할때 매번 새로운 객체로 설정할 수 있습니다.
Person person = new Person("str1"); //초기 객체 생성 person = new Person("str2"); //참조 변경 (기존 "str1" 객체는 메모리에 유지됨)
이 논리로 String 을 생각해보시면됩니다.
String str = "Hello"; //"Hello" 라는 객체를 생성 ex.new Person("Hello") str = str + " World"; //리터럴 "Hello" 와 "World" 를 합친 객체 생성 ex. new Person("Hello World")
결국 위 코드의 내부 구조는 StringBuilder 를 통해서 합쳐지는것 입니다.
String str = "Hello"; StringBuilder sb = new StringBuilder(); sb.append(str); // 기존 문자열 "Hello"를 추가 sb.append(" World"); // 새 문자열 " World"를 추가 String result = sb.toString(); // 최종 결합된 문자열 "Hello World"를 생성
이 되는것입니다.
그렇기에 String 은 불변이고 매번 새로운 객체를 만들기에 비용도 계속 소모하고 메모리도 소모하게 되는것입니다.
생성자
- 다양한 생성자가 존재하며, 문자열 리터럴, 문자 배열, 문자열 버퍼 등 다양한 형태로부터 문자열 객체를 생성할 수 있습니다.
public String(String original) { this.value = original.value; this.hash = original.hash; } public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } public String(char value[], int offset, int count) { this.value = Arrays.copyOfRange(value, offset, offset + count); }
메서드
- String 클래스는 다양한 메서드를 제공하여 문자열을 조작하고 정보를 추출할 수 있습니다
- substring:
- substring 메서드는 주어진 시작 인덱스와 끝 인덱스 사이의 문자열을 반환합니다. 새로운 String 객체를 생성하지만, Java 7 이전 버전에서는 내부 배열을 공유하여 메모리 효율성을 높였습니다. Java 7 이후로는 부분 배열을 복사하는 방식으로 구현되어 있습니다.
- charAt:
- charAt 메서드는 주어진 인덱스의 문자를 반환합니다. 인덱스 범위를 검사하고 유효한 경우 해당 인덱스의 문자를 반환합니다.
- indexOf:
- indexOf 메서드는 주어진 문자나 문자열이 처음으로 나타나는 위치를 반환합니다. 내부적으로 배열을 순회하며 검색을 수행합니다.
스트링 리터럴 풀(String Literal Pool)
Java 에는 String 을 자주 사용한다, 이 말인 즉슨 String 만큼 자주 사용하는 객체가 없기에 String 만을위한 JVM의 기능을 추가하였는데, 그게 String Literal Pool(스트링 리터럴 풀)이다.
String Literal Pool 을 사용하는 이유는 우선 Java는 동일한 값을 가지는 문자열 리터럴을 재사용하여 메모리 효율성을 높이고, 문자열 리터럴은 힙 메모리의 특별한 영역에 저장되어, 동일한 리터럴이 사용될 때마다 새 객체를 생성하지 않고 기존 객체를 재사용한다.
출처 : https://www.digitalocean.com/community/tutorials/what-is-java-string-pool 그렇기에 위에 String 은 객체를 참조하는것이고 이 객체는 리터럴풀의 같은곳을 참조하기 때문에 == 이 true 뿐만 아니라 hashcode() 도 동일하다, 완벽하게 같은 객체라는것이다.
public class StringLiteralPoolExample { public static void main(String[] args) { // 리터럴 문자열 String str1 = "Hello"; String str2 = "Hello"; // new 키워드를 사용하여 문자열 객체 생성 String str3 = new String("Hello"); // == 연산자로 참조 비교 System.out.println("str1 == str2: " + (str1 == str2)); // true System.out.println("str1 == str3: " + (str1 == str3)); // false // equals 메서드로 값 비교 System.out.println("str1.equals(str3)); // true // intern 메서드를 사용하여 리터럴 풀에 추가 String str4 = str3.intern(); // == 연산자로 참조 비교 System.out.println("str1 == str4: " + (str1 == str4)); // true } }
1. 문자열 리터럴 풀의 개념
- 리터럴 풀: 문자열 리터럴 풀은 JVM의 힙 메모리 내의 특수한 영역으로, 동일한 문자열 리터럴이 여러 번 사용될 때 하나의 객체를 공유합니다.
- 리터럴: 소스 코드 내에 하드코딩된 문자열 값. 예를 들어 "Hello"와 같은 값이 리터럴입니다.
2. 리터럴 풀의 동작 방식
- 리터럴 문자열의 저장:
- 소스 코드에서 문자열 리터럴이 처음 사용될 때, JVM은 이 문자열을 리터럴 풀에 저장합니다.
- 동일한 리터럴이 다시 사용될 때, JVM은 새로운 객체를 생성하지 않고 리터럴 풀에 저장된 기존 객체를 재사용합니다.
- 리터럴 풀에 저장된 객체의 참조:
- 동일한 문자열 리터럴을 사용하는 모든 변수는 리터럴 풀의 동일한 객체를 참조합니다.
- 이를 통해 메모리 사용을 줄이고, 문자열 비교 시 참조 비교를 통해 빠르게 비교할 수 있습니다.
3. intern 메서드
- intern 메서드: 문자열 객체를 리터럴 풀에 추가하고, 리터럴 풀에 이미 존재하는 경우 해당 객체를 반환합니다.
- 사용 예: 동적으로 생성된 문자열이 리터럴 풀의 객체와 동일한 참조를 가지도록 하려면 intern 메서드를 사용할 수 있습니다.
String str = new String("Hello"); String internedStr = str.intern(); String literalStr = "Hello"; System.out.println(internedStr == literalStr); // true
4. 리터럴 풀의 관리
- 기본 동작: JVM은 소스 코드에서 문자열 리터럴을 발견하면 자동으로 리터럴 풀에 저장합니다.
- intern 메서드: 동적으로 생성된 문자열을 리터럴 풀에 추가할 수 있습니다.
- 메모리 관리: 리터럴 풀에 저장된 문자열은 JVM이 종료될 때까지 메모리에 유지됩니다.
5. 리터럴 풀의 성능과 메모리 효율성
- 성능 향상: 리터럴 풀을 사용하면 문자열 비교 시 참조 비교(==)를 통해 빠르게 비교할 수 있습니다.
- 메모리 절약: 동일한 문자열 리터럴을 중복 저장하지 않음으로써 메모리 사용을 줄일 수 있습니다.
7. 리터럴 풀의 변경 사항 (Java 7 이후)
- Java 7 이전: 리터럴 풀은 PermGen 영역에 저장되었습니다.
- Java 7 이후: 리터럴 풀은 힙 메모리에 저장되어 PermGen의 한계로 인한 문제를 피할 수 있게 되었습니다. 이는 대규모 애플리케이션에서 특히 유용합니다.
주요 메서드
이제 String 클래스에 대해서 자세하게 알아봤으니, 주요 메서드에 대해 짚어볼 시간이다.
replaceAll()
replaceAll 메서드는 Java의 String 클래스에서 정규 표현식을 사용하여 문자열 내의 특정 패턴을 다른 문자열로 대체하는 데 사용됩니다. 이 메서드는 문자열 내에서 패턴과 일치하는 모든 부분을 찾아서 대체합니다.
public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement); }
내부 코드를 보면 결국 정규표현식의 matcher 클래스의 replaceAll 을 사용하는것을 볼 수 있다.
정규표현식의 내부 코드를 보면 결국 인덱스를 검색해서 검색 시작시점과 종료시점사이에 StringBuilder 를 가지고 문자열을 붙이는것을 볼 수 있다.
equals()
String 에서 사실상 가장 많이 사용되는 메소드이지 않을까 싶다.
public boolean equals(Object anObject) { if (this == anObject) { return true; } return (anObject instanceof String aString) && (!COMPACT_STRINGS || this.coder == aString.coder) && StringLatin1.equals(value, aString.value); } @IntrinsicCandidate public static boolean equals(byte[] value, byte[] other) { if (value.length == other.length) { for (int i = 0; i < value.length; i++) { if (value[i] != other[i]) { return false; } } return true; } return false; }
결국에는 for 문을 돌아서 일일히 확인한다.
Java의 String 클래스에서 equals 메서드를 사용할 때, 문자열을 비교하는 데 사용되는 coder는 문자열 데이터를 인코딩하는 방식을 나타낸다. coder는 Java 9에서 String 클래스의 내부 구현에 도입된 개념으로, 문자열 데이터를 효율적으로 저장하고 처리하기 위해 사용된다.
추가
JDK9 이후 char 저장 방식 변경
Java 9 이전에는 String 클래스가 내부적으로 char 배열을 사용하여 문자열 데이터를 저장했는데. Java 9에서는 메모리 사용을 최적화하고 성능을 개선하기 위해 byte 배열과 coder 필드를 사용하여 문자열 데이터를 저장하는 새로운 방식이 도입되었다.
Java 8까지 String 클래스는 char[] 배열을 사용하여 문자열 데이터를 저장했는데, 각 char는 16비트(2바이트)를 차지 한다. 예를 들어, "Hello"라는 문자열을 저장하려면 10바이트(5 * 2) + 객체 오버헤드가 필요하다.
Java 9부터 String 클래스는 byte[] 배열과 coder 필드를 사용하여 문자열 데이터를 저장한다. 이 방식에서는 문자열이 LATIN1(ISO-8859-1) 인코딩으로 저장될 수 있는 경우, 즉 모든 문자가 1바이트로 표현될 수 있는 경우, byte[] 배열로 저장된다. 그렇지 않은 경우에는 UTF-16 인코딩을 사용하여 byte[] 배열로 저장된다.
String str = "Hello"; // 내부적으로 'H', 'e', 'l', 'l', 'o'를 저장하는 char[] 배열 (10바이트 사용) String str = "Hello"; // 내부적으로 'H', 'e', 'l', 'l', 'o'를 저장하는 byte[] 배열 (5바이트 사용) + coder (1바이트)
'CS' 카테고리의 다른 글
Java) 컴파일, 인터프리터 그리고 자바 (0) 2024.11.10 Java) StringBuilder 와 StringBuffer (0) 2024.08.07 Java) 정규표현식 (0) 2024.08.05 Java) 가비지 컬렉션 (0) 2024.08.04 - final class String