https://www3.ntu.edu.sg/home/ehchua/programming/java/J3d_String.html
참조 원문입니다. 원문의 내용을 번역 해보고 추가로 jdk를 열어서 String class 등을 분석 해보았습니다.
String class의 간단한 요약
자바의 String은 불변하는 Unicode character들을 가지고 있다. C/C++에서는 문자열이 단순히 char의 배열인데 반해, 자바는 java.lang안에 있는 String 클래스의 객체를 의미한다. 일반 클래스와는 달리 자바의 String 클래스는 특별하다.
String은 "Hello, world!"와 같은 쌍따옴표로 감싸진 문자열 리터럴과 관련있다. 사용자는 생성자를 사용하는 대신 단순히 위와 같은 문자열 리터럴을 직접 String 변수에 대입할 수 있다.
cf) 리터럴이란?
리터럴은 변수에 넣는 데이터를 말한다. 데이터 자체를 의미하기 때문에 '상수'와 차이가 있다. 상수는 변하지 않는 변수를 의미하기 때문이다.
'+' 연산자는 String 끼리 붙이는 연산을 하기 위해 오버로딩 되어 있다.
String 은 불변이다. 그 말은 한번 초기화 된 객체는 내용을 바꿀 수가 없다는 것이다. 예를들어, toUpperCase() 메서드는 기존의 String 객체의 내용을 바꾸는 것이 아니라 새로 String 객체를 생성한다.
String 은 매우 특별하다
String은 자바에서 특별대우를 받는다. 왜냐하면 프로그램 상에서 매우 빈번히 사용 되기 때문이다. 그러므로 efficiency(효율성)이 매우 중요하다!
자바를 만든 사람들은 객체지향 언어를 설계할 때 모든 것을 객체로 만들지 않고 원시형 (primitive type)을 보유함으로써 언어의 성능 개선을 꾀했다. 원시형은 스택영역에 저장이되는데 그것은 공간을 덜 차지하고 조작하는데 비용이 적게 든다. 반면에, 객체는 힙 영역에 저장이 되는데 그것은 메모리 관리가 복잡하고 공간을 더 차지하게 된다.
성능상의 이유로, 자바의 String은 원시형과 클래스 사이의 형태로 설계되었다. String의 특별한 기능들은 다음과 같다.
- '+' 연산자(원시 값을 더하는 연산자, 예를들어 int나 double)는 String 객체를 조작하기 위해 오버로딩 되었다. 자바는 연산자 소프트웨어 공학 관점에서 오버로딩을 지원하지 않는다. C++과 같이 연산자 오버로딩을 지원하는 언어에서는 '+' 연산자를 빼기 연산이 되게 오버로딩을 할 수 있는데 이것은 쓰레기 코드를 만드는 결과를 초래한다. 자바에서는 오직 문자열 더하기 연산을 위해 오직 '+'만이 오버로딩 되어 있다.
- String 의 초기화는 직접 문자열 리터럴로 초기화 하든지 (원시값 초기화 처럼 예를들어, int a = 10;) 아니면 new 연산자로 초기화를 한다. 하지만 new 연산자로 초기화하는 방법은 추천하지 않는다.
그 이유를 살펴보자.
String 리터럴은 공통 pool에 저장이 된다. 이 영역은 저장공간을 아끼기 위해서 같은 내용의 문자열은 공유를 하게끔 만든다. 반면에 String 객체는 힙 영역에 저장이 되고 같은 내용의 문자열이라도 공유를 하지 않는다.
String s1 = "Hello"; // String literal
String s2 = "Hello"; // String literal
String s3 = s1; // 같은 참조
String s4 = new String("Hello"); // String object (객체)
String s5 = new String("Hello"); // String object (객체)
그렇기 때문에 s1 == s2로 문자열의 내용을 비교를 하는 것은 사실은 논리 에러이다. == 연산자는 주소를 비교하기 때문이다. 위의 그림에서 s4와 s5의 내용 비교는 equals를 통해서 true 반환이 가능하다.
String은 불변이다
문자열 리터럴은 공통 풀에 공유 데이터로 저장이 되기 때문에 자바의 String은 불변 (immutable)으로 디자인이 되었다. 즉, String이 만들어지면 수정이 불가하다는 말이다. 그렇지 않으면 다른 String의 레퍼런스가 같은 메모리 주소를 가리키고 있다가 내용이 바뀌게 된다면 영향을 받기 때문에 예측불가 상황이 발생한다. 위에서 말한 것 처럼 toUpperCase() 메서드는 String의 내용을 수정할 것 같지만 사실은 완전히 새로운 String 객체를 생성해 반환한다. 원본 String 객체는 다른 참조들이 없다면 메모리에서 해제되고 결과적으로 가비지 콜렉트가 된다.
String은 불변이기 때문에 String을 자주 조작하면 효율적이지 않게 된다.
// 비효율적인 코드
String str = "Hello";
for (int i = 1; i < 1000; ++i) {
str = str + i;
}
String이 계속 수정되어야 한다면 StringBuffer나 StringBuilder를 사용해야 한다.
StringBuffer & StringBuilder
JDK는 변환가능한 문자열을 지원하기 위해서 StringBuffer 와 StringBuilder를 제공한다. StringBuffer 와 StringBuilder는 여타 다른 일반적인 객체와 같이 힙 영역에 생성되고 공유되지 않는다. 그러므로 변화에 대해서 사이드 이펙트가 발생하지 않는다.
StringBuilder는 JDK 1.5부터 소개가 되었다. StringBuffer 클래스와 같지만 StringBuilder는 멀티 스레드 환경에서 동기화를 지원하지 않는다. 그러나 싱글 스레드 환경에서는 동기화 오버헤드가 없기 때문에 더 효율적이다.
문자열 연산에 있어서 String 의 '+' 연산보다 StringBuilder의 append를 사용하는 것이 효율적이다.
하지만 사실 JDK 컴파일러는 String 연산에서 append 연산을 수행하여 준다.
예를들어,
String msg = "a" + "b" + "c";
String msg = new StringBuffer().append("a").append("b").append("c").toString();
첫 번째 연산을 수행하는 것은 두 번째 연산으로 바꾸어 준다.
String class 내부구현
위 내용도 확인할 겸, 내부적으로 어떻게 구현되어 있는지 열어 보았다. 그 전에 oracle 문서를 찾아보자.
위에서 설명한 문자열 리터럴을 assign 하는 모습이다. String str = "abc";는 char의 배열을 선언하고 new String(char 배열)과 같다고 한다. 정말 그런지 살펴보자.
String 클래스는 final로 선언이 되어있다. 그렇기 때문에 저 String 클래스를 상속해서 사용할 수 없다는 것을 의미한다. 변종을 만드는 것을 애초에 차단시키는 모습이다. 그리고 직렬화를 위한 Serializable, 비교를 위한 Comparable 그리고 read-only를 위한 CharSequence 인터페이스를 구현한다. CharSequence는 열어보니 length() 나 charAt()과 같은 문자열 조회 기능을 추상화한 인터페이스였다.
멤버 변수는 char의 배열로 가지고 있다. 그리고 hash 값을 int 형으로 가지고 있는 것을 볼 수 있다.
그래서 흔히 아는 charAt 메서드는 배열의 인덱스로 접근해서 빠르게 리턴하게 된다.
Abstract String Builder
StringBuffer와 StringBuilder는 AbstractStringBuilder 추상 클래스를 상속한다.
이 추상 클래스는 문자열 필드와 카운트 필드를 가지고 있다. 그리고 StringBuilder와 StringBuffer에서 자주 쓰이는 append 메서드를 가지고 있다. 둘이서 AbstractStringBuilder의 append를 오버라이드해서 사용을 한다.
붙이려는 문자열이 null이면 appendNull()함수를 호출한다.( appendNull()함수를 보면 실제로 'n', 'u', 'l', 'l' 문자를 붙여버린다) 그리고 붙이려는 문자열의 길이를 구해서 ensureCapacityInternal()를 호출하는데 이것은 객체의 char[] 배열의 용량을 확인하여 붙이려는 문자열을 포함시켰을 때 용량을 넘어서게되면 확장을 해준다. 그리고 getChars를 보면 세 번째 인자로 value가 들어가게 되는데 이 value는 클래스의 주요 필드 값으로 보유하고 있는 문자열을 의미한다. 그럼 저 value에 str(붙이려는 문자열) 이 붙어서 나오게 된다. 그리고 count를 늘려주게 된다.
StringBuffer의 append는 thread-safe하기 위해 이렇게 synchronized를 붙인 모습이다.
추가적인 내용은 추후 append() 를 하겠다.
'프로그래밍 > Java' 카테고리의 다른 글
[Java] Thread(스레드)에 관한 고찰 - 스레드 관련 코드를 어떻게 짜야할까 (1) | 2019.10.27 |
---|---|
[Servlet] 웹서버 vs 애플리케이션 서버 vs 서블릿 컨테이너 - Tomcat 은 서블릿 컨테이너일까? (3) | 2019.10.09 |
[Java] 자바 자료형(primitive, reference) 에 대한 고찰 - 자바 Integer에 캐시가 있다고? (1) | 2019.07.02 |
[JDBC] MySQL 드라이버 Class.forName()의 비밀 - 로드만 했을 뿐인데 getConnection()이 가능하다고? (6) | 2019.06.04 |
[Java] 추상클래스(Abstract Class)와 인터페이스(Interface)에 대한 고찰 (0) | 2018.11.28 |