자바에서는 문자열 데이터를 저장하기 위해 String 클래스 타입을 사용한다.
어떻게 다루느냐에 따라 어플리케이션의 성능이 차이가 난다.
메모리 관점
→ stack 영역이 아닌 객체와 같이 힙에서 문자열 데이터가 생성되고 다뤄진다.
→ String name = "홍길동" → name 이라는 지역 변수는 Stack 영역에 존재하고, "홍길동"이라는 문자열의 위치 값을 가지고 있다. 해당 문자열은 힙 영역에서 생성된다.
String 클래스의 기본적인 특징
기본적으로 자바에서는 String 객체의 값은 변경할 수 없다. (Immutable)
→ 기존의 문자열에 다른 문자열을 더한다면 새로운 문자열 데이터가 생성됨
→ 즉, 문자열 값 자체는 불변이라 변경할 수 없기 때문에 새로운 문자열 데이터 객체를 대입하여 새로운 공간에 값이 할당됨 → 이런 관리 문제를 해결하기 위해 StringBuilder 와 같은 클래스를 제공함.
String a = "Hello";
System.out.println(a.hashCode()); // 69609650
a+= "Java Programming";
System.out.println(a.hashCode()); // -351927155
// 새로운 문자열 데이터 a가 생성되어 메모리의 힙 영역에 할당되었음.
hashCode() : Object 클래스에 존재하는 메소드로, 객체의 메모리 번지를 이용해서 해시코드를 만들어 리턴하는 메소드
JVM > Heap > String Pool 에 문자열이 저장됨.
🤔자바의 String은 왜 Immutable로 설계되었을까?
- 보안적인 측면 : 참조 값을 변경하여 어플리케이션 보안 문제가 생길 수 있으므로 불변 처리함
- 멀티 쓰레드 환경에서 동기화 문제가 발생하지 않는다.
- JVM에서는 따로 힙모메리 영역 안에 String Constant Pool로 관리하고 있다. 문자열을 상수화하여 다른 변수나 다른 객체들과 공유할수 있도록 제공하고 있다.
- 스레드 안전성 (Thread Safety):
- 불변 객체는 여러 스레드에서 동시에 접근해도 상태가 변경될 염려가 없으므로 별도의 동기화 메커니즘이 필요 없습니다. 이는 멀티스레드 환경에서 String 객체를 안전하게 공유할 수 있게 하여 동시성 문제를 크게 줄여줍니다.
- 만약 String이 가변적이었다면, 한 스레드가 문자열을 수정하는 동안 다른 스레드가 그 문자열을 읽는 경우 예측 불가능한 결과가 발생할 수 있습니다.
- 보안 (Security):
- String은 파일 경로, 네트워크 연결 주소, 데이터베이스 연결 정보, 사용자 비밀번호 등 민감한 정보를 저장하는 데 자주 사용됩니다.
- 만약 String이 가변적이라면, 보안 검증 후에 문자열의 내용이 예기치 않게 변경될 수 있습니다. 예를 들어, 파일 경로가 유효하다고 검증된 후 악성 경로로 변경되어 시스템에 접근하는 공격에 취약해질 수 있습니다. 불변성은 이러한 종류의 공격을 방지하는 데 도움을 줍니다.
- 성능 및 효율성 (Performance and Efficiency) - 특히 문자열 풀 (String Pool):
- 자바는 String 리터럴을 위한 특별한 메모리 영역인 "문자열 풀(String Pool)"을 유지합니다. 동일한 내용을 가진 문자열 리터럴이 있을 때, JVM은 새로운 객체를 생성하는 대신 풀에 있는 기존 객체를 재사용합니다.
- String이 불변이기 때문에 이러한 공유가 가능합니다. 만약 가변적이었다면, 한 곳에서 문자열이 변경되면 다른 곳에서 참조하는 모든 문자열에 영향을 미치게 되어 공유가 불가능하거나 매우 복잡해졌을 것입니다.
- 불변성은 hashCode() 값이 한 번 계산되면 캐시될 수 있도록 합니다. String은 HashMap의 키 등으로 자주 사용되는데, 이 때 해시 코드를 효율적으로 재사용할 수 있어 성능에 이점이 있습니다.
- 캐싱 (Caching):
- String의 해시 코드는 한 번 계산되면 변경될 필요가 없으므로 캐시될 수 있습니다. 이는 String 객체가 HashMap이나 HashSet과 같은 해시 기반 컬렉션의 키로 사용될 때 성능을 향상시킵니다.
- 보정된 해시 코드 (Hash Code Reliability):
- String 객체가 Map의 키로 사용될 때, 그 객체의 hashCode() 값은 변경되지 않아야 합니다. 만약 String이 가변적이고 Map에 키로 저장된 후에 내용이 변경된다면, 해당 키를 다시 찾을 수 없게 되는 문제가 발생합니다. 불변성은 이 문제를 방지합니다.
- 코드 가독성 및 유지보수성 (Code Readability and Maintainability):
- 불변 객체는 상태가 변하지 않으므로 코드의 흐름을 추적하고 이해하기 더 쉽습니다. 이는 버그를 줄이고 코드의 유지보수성을 높이는 데 기여합니다.
🤔자바의 문자열은 왜 두 가지 생성 방식을 지원할까?
1. 문자열 리터럴 방식 (String Literal)
예시: String s = "Hello";
동작 방식:
- JVM은 String 리터럴을 만나면 먼저 **문자열 상수 풀(String Constant Pool)**이라는 특별한 메모리 영역을 확인합니다.
- 만약 풀에 동일한 내용을 가진 문자열이 이미 존재한다면, 새로운 객체를 생성하지 않고 풀에 있는 기존 객체의 참조를 반환합니다.
- 만약 풀에 동일한 내용의 문자열이 없다면, 새로운 String 객체를 풀에 생성하고 그 참조를 반환합니다.
장점:
- 메모리 효율성: 동일한 문자열에 대해 여러 번 객체를 생성하지 않고 재사용하므로 메모리 사용을 최적화할 수 있습니다.
- 성능: 기존 객체를 재사용할 수 있어 객체 생성 오버헤드를 줄입니다.
- 간결성: 코드가 더 간결하고 읽기 쉽습니다.
주요 사용 목적:
- 대부분의 경우 문자열을 선언하고 초기화할 때 사용합니다.
- 동일한 문자열이 여러 번 사용될 가능성이 있는 경우에 특히 효율적입니다.
2. new 연산자를 통한 생성 방식
예시: String s = new String("Hello");
동작 방식:
- new 연산자를 사용하면 문자열 상수 풀을 확인하지 않고, 힙(Heap) 메모리 영역에 항상 새로운 String 객체를 생성합니다.
- 만약 new String("Hello")와 같이 리터럴을 인자로 전달하면, "Hello" 리터럴은 문자열 상수 풀에 생성되거나 재사용될 수 있지만, new String()에 의해 별개의 새로운 "Hello" 객체가 힙에 생성됩니다.
장점:
- 명시적인 새로운 객체 생성: 항상 새로운 String 객체를 생성해야 할 필요가 있을 때 사용합니다. 예를 들어, 두 문자열의 내용이 같더라도 메모리상에서 완전히 다른 객체로 존재해야 함을 보장해야 할 때 유용합니다.
단점:
- 메모리 비효율성: 동일한 내용의 문자열이라도 new를 사용할 때마다 새로운 객체가 생성되므로 메모리 사용량이 늘어날 수 있습니다.
- 성능 저하: 객체 생성 오버헤드가 발생합니다.
주요 사용 목적:
- 두 문자열 객체가 내용이 같더라도 서로 다른 메모리 주소를 가지도록 강제해야 할 때 (예: 특정 객체 동일성 비교가 중요한 경우).
- 매우 드물지만, 문자열 상수 풀에 너무 많은 문자열이 쌓여 메모리 문제가 발생할 가능성을 피하고 싶을 때 (일반적인 애플리케이션에서는 거의 고려되지 않음).
- 자바가 두 가지 문자열 생성 방식을 제공하는 것은 개발자가 특정 시나리오에 따라 메모리 사용과 성능을 최적화할 수 있도록 유연성을 주기 위함입니다. 대부분의 경우 문자열 리터럴 방식이 효율성과 간결성 면에서 더 권장되며, new String()은 특정 목적(예: 명시적으로 새로운 객체 생성 보장)을 위해 제한적으로 사용됩니다.
결론:
자바가 두 가지 문자열 생성 방식을 제공하는 것은 개발자가 특정 시나리오에 따라 메모리 사용과 성능을 최적화할 수 있도록 유연성을 주기 위함입니다. 대부분의 경우 문자열 리터럴 방식이 효율성과 간결성 면에서 더 권장되며, new String()은 특정 목적(예: 명시적으로 새로운 객체 생성 보장)을 위해 제한적으로 사용됩니다.
public class InternEx {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = new String(new char[]{'H','e','l','l','o'}).intern();
System.out.println(str1 == str2);
// 해당 코드의 의미는 무엇인가?
// intern 메소드의 기능을 확인하는 코드로 이미 literal constant pool에 존재하는 문자열인지 확인하고, 존재한다면 그 주소값을 str2에 할당해줌
// 그러므로 true를 반환함.
String str3 = new String("Hi");
String str4 = "Hi";
str3 = str3.intern();
System.out.println(str3 == str4); // true
}
}
intern() 메소드
- intern() : 자바에서 문자열을 최적화하여 관리하기 위한 메소드
- 문자열을 리터럴로 선언할 경우 내부적으로 String의 intern() 메소드가 호출이 된다.
- 해당 리터럴이 문자열 상수 풀(Literal constant pool)안에 존재하는지 확인,
- 풀에 존재한다면: 풀에 있는 해당 문자열 객체의 참조(주소)를 반환합니다. 즉, 현재 객체는 버려지고 풀에 있는 기존 객체를 재사용합니다.
- 풀에 존재하지 않는다면: 현재 String 객체를 문자열 상수 풀에 추가하고, 이 객체의 참조를 반환합니다.
public class InternEx {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = new String(new char[]{'H','e','l','l','o'}).intern();
System.out.println(str1 == str2);
// 해당 코드의 의미는 무엇인가?
// intern 메소드의 기능을 확인하는 코드로 이미 literal constant pool에 존재하는 문자열인지 확인하고, 존재한다면 그 주소값을 str2에 할당해줌
// 그러므로 true를 반환함.
String str3 = new String("Hi");
String str4 = "Hi";
str3 = str3.intern();
System.out.println(str3 == str4); // true
}
}
자주 활용되는 메소드
- charAt(), length(), replace(), substring() , indexOf() , contains() , split()
- Class Character의 메소드
- isUpperCase() : 대문자인지 소문자인지
- isDigit() : 숫자인지 아닌지
StringBuffer 클래스
- + 연산자를 이용하여 String 인스턴스의 문자열을 결합하면, 내용이 합쳐진 새로운 String 인스턴스가 생성됨.
- 문자열을 많이 결합하면 할수록 메모리 낭비, 속도도 느려지는 단점이 있다.
- StringBuffer 클래스는 클래스 내부에 버퍼(buffer. 데이터를 임시로 저장하는 메모리)라는 독립적인 공간을 가지고 있어 문자열을 바로 바로 추가할 수 있어 공간의 낭비 없이 문자열 연산(추가, 수정, 삭제) 속도를 빠르게 처리할 수 있다.
- 문자열의 수정이 빈번한 경우에 사용하는 것이 적합하며, 네트워크 상에서 데이터를 송수신하는 경우는 불변타입을 사용하는 것이 적합하다. (Ex. json 타입)
StringBuffer sb1 = new StringBuffer();
sb1.append("hello");
sb1.append(" ");
sb1.append("Java programming");
sb1.append("!!!");
String result1 = sb1.toString();
System.out.println(result1);
String str = "abcdefg";
StringBuffer sb = new StringBuffer(str);
System.out.println("초기 상태 : " + sb);
// StringBuffer를 String 타입으로 변환
System.out.println("초기 상태 : " + sb.toString());
// str문자열에서 cd를 출력하세요
System.out.println("문자열 추출 :" + sb.substring(2,4));
//str문자열에 "2" 추가하세요,
System.out.println("문자 추가 : "+sb.insert(2, "추가"));
// sb에서 문자열을 삭제
System.out.println("문자 삭제 : " +sb.delete(2,4));
// sb에 문자 붙이기 : append()
System.out.println("문자 붙이기 : "+sb.append("hijl"));
// sb의 길이 : length()
System.out.println("문자열의 길이 : "+sb.length());
// buffer 용량
System.out.println("용량 : " + sb.capacity());
// 문자열의 역순 : reverse()
System.out.println("문자열의 역순 : "+sb.reverse());
// 현상태 문자열 확인
System.out.println("마지막 상태 체크: " + sb);