Posts :coffee: [Java] String 클래스
Post
Cancel

:coffee: [Java] String 클래스

java.lang.String

  • String의 구조는 원래 char 배열이었으나, JDK9부터 byte 배열로 바뀌었다. 순수한 ISO-8859 로 되어있다면 각 글자를 1바이트로, ISO-8859에 해당하지 않는(한글 등) 문자가 하나라도 포함되어 있으면 2바이트로 저장한다.
  • char 배열이었을 때는 어떤 언어든지 무조건 UTF-16(2바이트)으로 저장하였다.

  • new String()을 하게 되면 hash값을 저장하기 위한 변수(int)와 byte 배열이 인스턴스 변수가 heap에 생성된다.
  • 같은 문자열이 있는지 검사하지 않고 무조건 저장한다.

String 객체

1
2
3
4
String s1 = new String("Hello");
String s2 = new String("Hello");

System.out.println(s1 == s2); //false
  • 힙에 문자 코드를 저장할 메모리를 만들고 그 주소를 리턴한다.
  • 내용물의 동일 여부를 검사하지 않고 무조건 인스턴스를 생성한다.
  • 가비지가 되면 가비지 컬렉터에 의해 제거된다.

문자열 리터럴

  • 문자열 상수풀(String constant pool) 메모리 영역에 String 인스턴스를 생성한다.
  • 내용물이 같으면 기존 인스턴스 주소를 리턴한다.
  • 메모리 절약을 위해 중복 데이터를 갖는 인스턴스 생성하지 않는다.
  • JVM이 끝날 때까지 메모리에 유지된다.
  • 컴파일러가 자동으로 new String() 을 해서 String 인스턴스를 만드는데, 이때 인스턴스가 힙에 생성되는 게 아니라 상수풀에 저장된다.
1
2
3
String x1 = "Hello";
String x2 = "Hello";
System.out.println(x1 == x2); //true

인스턴스 메서드

intern()

  • 지정된 String 객체를 상수풀에서 찾는다
  • 있으면 그 String 객체의 주소를 리턴한다.
  • 없으면 상수풀에 String 객체를 생성한 후 그 주소를 리턴한다.
1
2
3
4
5
6
String s1 = new String("Hello");
String s2 = s1.intern();
String s3 = "Hello";

System.out.println(s1 == s2); //false
System.out.println(s2 == s3); //true

equals(): overriding

  • Object에 정의되어 있는 메서드 equals()는 인스턴스가 같은지 비교한다.

  • String 클래스는 이 equals()를 오버라이딩하여 인스턴스가 아니라 문자열이 같은지 비교한다.
  • equalsIgnoreCase() : 대소문자 구분 없이 문자열 비교
  • StringBuffer 클래스: Object에서 상속 받은 equals()를 오버라이딩 하지 않아 이때 equals()는 인스턴스가 같은 지 비교한다.
1
2
3
4
5
// StringBuffer 에 들어 있는 문자열을 비교하려면?
// StringBuffer에서 String을 꺼내 비교하라!
System.out.println(b1.toString().equals(b2.toString()));
System.out.println(b1.toString());
System.out.println(b2.toString());

hashCode(): overriding

  • Object의 hashCode(): 인스턴스마다 리턴값이 다르다.
  • String의 hashCode(): 문자열이 같으면 같은 hashCode()를 리턴하도록 오버라이딩하였다.
  • 재정의 이유
    • 문자열이 같은 경우 같은 객체로 다루기 위해
    • HashSet에서 객체를 저장할 때 이 메서드의 리턴값으로 저장 위치를 계산한다.
    • HashMap이나 HashTable에서는 Key를 다룰 때 이 메서드의 리턴값을 사용한다.

toString(): overriding

  • Object의 toString(): “클래스명@해시값”을 리턴한다.
  • Sring의 toString(): this 주소를 그대로 리턴한다.
1
2
3
String s1 = new String();
String s2 = s1.toString();
System.out.println(s1 == s2); // true

다형적 변수와 형변환

1
2
3
4
Object obj = new String("Hello"); // 인스턴스 주소가 100이라 가정하자
String x1 = (String) obj; // x1 <= 100
String x2 = obj.toString(); // x2 <= 100
System.out.println(x1 == x2); // true
  • Object 타입 레퍼런스가 String 객체를 가리킬 때 Object 클래스의 멤버(필드와 메서드)만 사용할 수 있다.
  • 단 멤버는 실제 obj가 가리키는 클래스(String)부터 찾아 올라간다.
  • 위의 예의 경우 실제 가리키는 것은 String이기 때문에 오버라이딩한 toString() 메서드를 호출한다.
  • obj가 가리키는 원래 클래스의 메서드를 호출하고 싶다면?
    • 원래 타입으로 형변환한다.
    • 원래 타입의 레퍼런스에 저장한 다음 사용한다.

레퍼런스를 통해 메서드를 호출할 때, 레퍼런스가 가리키는 객체의 클래스부터 메서드를 찾아 올라간다.

1
2
3
4
5
6
7
8
9
10
11
// Object타입 레퍼런스가 가리키는 객체, String 클래스의 메서드인
// toLowerCase()를 호출하고 싶다면?

// 1. 원래 타입으로 형변환한다.
Object obj = new String("Hello");
String str = ((String)obj).toLowerCase();

// 2. 원래 타입의 레퍼런스에 저장한 다음 사용한다.
String x1 = (String) obj;
str = x1.toLowerCase();
System.out.println();

mutable vs immutable

Immutable 객체

  • String 객체는 immutable 객체이다.

  • 한번 인스턴스가 생성되면 값을 변경할 수 없다.

  • String 클래스의 메서드는 원본 인스턴스의 데이터를 변경하지 않는다. 다만 새로 String객체를 만들 뿐이다.

    • replace() , concat(): 새로운 문자열을 생성하여 그 주소를 리턴한다. 원본은 바뀌지 않는다.
1
2
3
4
5
6
7
String s1 = new String("Hello");

String s2 = s1.replace('l', 'x');
System.out.println("%s : %s\n", s1, s2); // 원본은 바뀌지 않는다.

String s3 = s1.concat(", world!");
System.out.println("%s : %s\n", s1, s3); // 원본은 바뀌지 않는다.
  • 단점: 메서드를 사용할 때마다 계속 새로 인스턴스를 만들어낸다. 레퍼런스로 가리키지 않는다면 이 데이터는 힙에 쌓여서 가비지가 된다. 따라서 mutable인 StringBuffer를 사용한다.

mutable 객체

  • StringBuffer 객체는 mutable 객체이다.
  • 인스턴스의 데이터를 변경할 수 있다.
  • 원래의 문자열을 변경하고 싶을 때 사용하는 클래스이다.
1
2
3
4
StringBuffer buf = new StringBuffer("Hello");
System.out.println(buf);

buf.replace(2, 4, "xxxx"); //원본을 바꾼다.

스태틱 메서드

  • String.format(): 지정된 위치에 값을 대입해서 문자열을 만든다.
  • String.valueOf(): 기본 타입의 값을 문자열로 만든다.
1
2
3
4
5
String s1 = String.format("%s님의 나이는 %d입니다.", "홍길동", 20);
System.out.println(s1);

String s2 = String.valueOf(true); // true => "true"
String s3 = String.valueOf(100); // 100 => "100"

다양한 생성자 활용

String 인스턴스 생성

  • 내부적으로 문자의 코드 값을 저장할 char 배열(버전 1.8까지) 또는 byte 배열(버전 1.9부터)을 생성한다.
  • 생성자에서 넘겨주는 값을 배열에 저장한다.
  • 만약 생성자에 아무것도 넘겨주지 않으면 빈 배열이 생성된다.
  • 방법
    • 1) 문자열 리터럴로 생성
    • 2) char 배열로 생성
    • 3) byte 배열로 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
String s2 = new String("Hello"); // 문자열 리터럴로 String 인스턴스 생성
System.out.printf("s2=%s\n", s2);

char[] chars = {'H', 'e', 'l', 'l', 'o'};
String s3 = new String(chars); // char 배열로 String 인스턴스 생성
System.out.printf("s3=%s\n", s3);

byte[] bytes =
    {(byte) 0xb0, (byte) 0xa1, (byte) 0xb0, (byte) 0xa2, 0x30, 0x31, 0x32, 0x41, 0x42, 0x43};
// 문자 코드 값이 저장된 바이트 배열로 String 인스턴스 생성
String s4 = new String(bytes);
System.out.printf("s4=%s\n", s4);
// 한글이 깨진다. 이유?
// => String 생성자는 파라미터로 받은 바이트 배열에 ISO-8859-1 문자 코드가 들어 있다가 간주한다.
// 즉 0xb0 0xa1 값이 한글 '가'가 아니라 0xb0와 0xa1 각각을 영어라 간주하고
// ISO-8859-1 에 정의된 문자표에 따라 유니코드(UTF-16)으로 바꿔 저장한다.
// 0xb0(ISO-8859-1) ==> 0x00b0(UTF-16)
// => 제대로 한글을 처리하려면?
// 생성자에 바이트 배열을 넘겨줄 때
// 배열에 들어 있는 코드 값이 어떤 문자표의 코드 값인지 알려줘야 한다.
//

String s5 = new String(bytes, "euc-kr");
System.out.printf("s5=%s\n", s5);

byte[] bytes2 =
    {(byte) 0xac, (byte) 0x00, (byte) 0xac, (byte) 0x01, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63};

// 바이트 배열에 들어 있는 코드는 무슨 문자표로 작성했는지 정확하게 알려줘야 한다.
// 그래야 자바의 문자 코드로 제대로 변경할 수 있을 것이다.
String s6 = new String(bytes2, "utf-16");
System.out.printf("s6=%s\n", s6);

byte[] bytes3 = {(byte) 0xea, (byte) 0xb0, (byte) 0x80, (byte) 0xea, (byte) 0xb0, (byte) 0x81,
    0x61, 0x62, 0x63};

String s7 = new String(bytes3, "utf-8");
System.out.printf("s7=%s\n", s7);
This post is licensed under CC BY 4.0 by the author.

💻 02. HTTP 캐싱

:coffee: [Java] wrapper 클래스

Loading comments from Disqus ...