[Java의 정석]제9장 java.lang패키지 - 2.String클래스

류명운

·

2014. 7. 3. 22:36

반응형
2. String클래스



기존의 다른 언어에서는 문자열을 char형의 배열로 다루었으나 자바에서는 문자열을 위한 클래스를 제공한다. 그 것이 바로 String클래스인데, String클래스는 문자열을 저장하고 이를 다루는데 필요한 메서드를 제공한다.
지금까지는 String클래스의 기본적인 몇 가지 기능만 사용해 왔지만, String클래스에는 문자열을 다루는데 유용한 메서드들이 많이 있다. 이제 String클래스에 대해서 자세히 알아보도록 하자.


2.1 String클래스의 특징

String클래스에는 문자열을 저장하기 위해서 문자형 배열 변수(char[]) value를 인스턴스 변수로 정의해놓고 있다. 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스변수(value)에 문자형 배열(char[])로 저장되는 것이다.


public final class String implements java.io.Serializable, Comparable {
/** The value is used for character storage. */
private char[] value;
...



한번 생성된 String인스턴스가 갖고 있는 문자열은 읽어 올 수만 있고, 변경할 수는 없다.
예를 들어 "a" + "b"와 같이 '+'연산자를 이용해서 문자열을 결합하는 경우 인스턴스내의 문자열이 바뀌는 것이 아니라 새로운 문자열("ab")이 담긴 String인스턴스가 생성되는 것이다.


String a = "a";
String b = "b";
String ab = a + b;


이처럼 덧셈연산자(+)를 사용해서 문자열을 결합하는 것은 매 연산 시 마다 새로운 문자열을 가진 String인스턴스가 생성되어 메모리공간을 차지하게 되므로 가능한 한 결합횟수를 줄이는 것이 좋다.
문자열간의 결합이나 추출 등 문자열을 다루는 작업이 많이 필요한 경우에는 String클래스 대신 StringBuffer클래스를 사용하는 것을 고려해보도록 한다.
String인스턴스와는 달리 StringBuffer인스턴스에 저장된 문자열은 변경이 가능하므로 하나의 StringBuffer인스턴스만으로도 문자열을 다루는 것이 가능하다.

[예제9-9] StringEx1.java

class StringEx1
{
public static void main(String[] args)
{
String str1 = "abc";
String str2 = "abc";

System.out.println(" String str1 = \"abc\";");
System.out.println(" String str2 = \"abc\";");

if(str1 == str2) {
System.out.println(" str1 == str2 ? true");
} else {
System.out.println(" str1 == str2 ? false");
}

if(str1.equals(str2)) {
System.out.println(" str1.equals(str2) ? true");
} else {
System.out.println(" str1.equals(str2) ? false");
}
System.out.println();

String str3 = new String("\"abc\"");
String str4 = new String("\"abc\"");

System.out.println(" String str3 = new String(\"abc\");");
System.out.println(" String str4 = new String(\"abc\");");

if(str3 == str4) {
System.out.println(" str3 == str4 ? true");
} else {
System.out.println(" str3 == str4 ? false");
}

if(str3.equals(str4)) {
System.out.println(" str3.equals(str4) ? true");
} else {
System.out.println(" str3.equals(str4) ? false");
}
}
}
[실행결과]
String str1 = "abc";
String str2 = "abc";
str1 == str2 ? true
str1.equals(str2) ? true

String str3 = new String("abc");
String str4 = new String("abc");
str3 == str4 ? false
str3.equals(str4) ? true

문자열을 만들 때는 두 가지 방법, 문자열 리터럴을 지정하는 방법과 String클래스의 생성자를 사용해서 만드는 방법이 있다. 이 예제는 문자열을 리터럴로 생성하는 것과 String클래스의 생성자를 사용해서 생성하는 것과의 차이를 보여 주기 위한 것이다.

equals(String s)를 사용했을 때는 두 문자열의 내용("abc")을 비교하기 때문에 두 경우 모두 true를 결과로 얻는다. 하지만, 각 String인스턴스의 주소값을 등가비교연산자(==)로 비교했을 때는 결과가 다르다. 리터럴로 문자열을 생성했을 경우, 같은 내용의 문자열들은 모두 하나의 String인스턴스를 참조하도록 되어 있다. 어차피 String인스턴스가 저장하고 있는 문자열은 변경할 수 없기 때문에 아무런 문제가 없다.



그러나, String클래스의 생성자를 이용한 String인스턴스의 경우에는 new연산자에 의해서 메모리할당이 이루어지기 때문에 항상 새로운 String인스턴스가 생성된다.
아래의 왼쪽 그림은 리터럴로 문자열을 생성했을 때의 상황이고, 오른쪽 그림은 생성자를 이용해서 문자열을 생성했을 때의 상황을 그림으로 나타낸 것이다.

[예제9-8] StringEx2.java

class StringEx2
{
public static void main(String args[]) {
String s1 = "AAA";
String s2 = "AAA";
String s3 = "AAA";
String s4 = "BBB";
}
}

위의 예제를 컴파일 하면 StringEx2.class파일이 생성된다. 이 파일의 내용을 16진 코드에디터로 보면 아래의 그림과 같다.


[참고] 일반 문서 편집기로도 StringEx2.class파일의 내용을 볼 수 있다.


우측 부분을 보면 알아볼 수 있는 글자들이 눈에 띨 것이다. 그 중에서도 "AAA""BBB"가 있는 것을 발견할 수 있을 것이다. 이와 같이 String리터럴들은 컴파일 시에 클래스파일에 저장된다.
위의 예제를 실행하게 되면 "AAA"라는 문자열을 담고 있는 String인스턴스가 하나 생성된 후, 참조변수 s1, s2, s3는 모두 이 String인스턴스를 참조하게 된다.


모든 클래스 파일(*.class)에는 constant pool이라는 상수값 목록이 있어서, 여기에 클래스 내에서 사용되는 문자열 리터럴과 상수값들이 저장되어 있다.

[예제9-9] StringEx3.java

class StringEx3
{
public static void main(String[] args)
{
String s1 = "AAA";
String s2 = new String("AAA");

if (s1==s2) {
System.out.println("s1==s2 ? true");
} else {
System.out.println("s1==s2 ? false");
}

s2 = s2.intern();
System.out.println("s2에 intern()을 호출한 후");

if (s1==s2) {
System.out.println("s1==s2 ? true");
} else {
System.out.println("s1==s2 ? false");
}

}
}
[실행결과]
s1==s2 ? false
s2에 intern()을 호출한 후
s1==s2 ? true

String클래스의 intern메서드는 String인스턴스의 문자열을 constant pool에 등록하는 일을 한다. 등록하고자 하는 문자열이 constant pool에 이미 존재하는 경우에는 그 문자열의 주소값을 반환한다.(그리 중요한 메서드는 아니지만, 많은 책들이 어렵게 설명하고 있기 때문에 참고로 추가하였다.)

위의 예제에서는 참조변수 s2가 가리키는 문자열이 이미 constant pool에 등록되어 있기 때문에, s2.intern()의 결과로 등록되어 있는 String인스턴스(문자열을 담고 있는)의 주소값을 얻게 된다. 그래서 참조변수 s1과 s2는 같은 String인스턴스를 가리키게 되어 s1==s2의 결과가 true가 되는 것이다.

1. String s1 = "AAA";
String s2 = new String("AAA");





2. s2 = s2.intern();



[예제9-10] StringEx4.java

class StringEx4
{
public static void main(String[] args)
{

String[] words = { new String("aaa"), new String("bbb"), new String("ccc") };

for(int i=0; i < words.length; i++) {
if(words[i].equals("ccc")) {
System.out.println("words에서 equals메서드로 찾았습니다.");
}
if(words[i] == "ccc") {
System.out.println("words에서 ==연산자로 찾았습니다.");
}
}

for(int i=0; i < words.length; i++) {
words[i] = words[i].intern();
}
System.out.println("<< String배열 words의 문자열에 intern메서드를 수행한 후 >>");
for(int i=0; i < words.length; i++) {
if(words[i].equals("ccc")) {
System.out.println("words에서 equals메서드로 찾았습니다.");
}
if(words[i] == "ccc") {
System.out.println("words에서 ==연산자로 찾았습니다.");
}
}
} // end of main
} // end of class
[실행결과]
words에서 equals메서드로 찾았습니다.
<< String배열 words의 문자열에 intern메서드를 수행한 후 >>
words에서 equals메서드로 찾았습니다.
words에서 ==연산자로 찾았습니다.

intern메서드가 수행된 String인스턴스는 equals메서드 뿐만 아니라 등가비교연산자(==)를 가지고도 문자열을 비교할 수 있다는 것을 보여 주는 예제이다.
일반적으로 문자열들을 비교하기 위해서 equals메서드를 사용하지만, equals메서드로 문자열의 내용을 비교하는 것보다는 등가비교연산자(==)를 이용해서 주소(4 byte)를 비교하는 것이 더 빠르다.

그래서 비교해야할 문자열의 개수가 많은 경우에는 보다 빠른 문자열 검색을 위해서 intern메서드와 등가비교연산자(==)를 사용하기도 한다.

[예제9-11] StringEx5.java

class StringEx5
{
static String s;
static String s2 = "";
public static void main(String[] args)
{
for(int i=1; i < 10; i++) {
s += i; // s = s + i;
s2 += i;
}
System.out.println(s);
System.out.println(s2);
}
}
[실행결과]
null123456789
123456789

이 예제는 1부터 9까지의 숫자를 문자열로 만들어서 덧붙이는 일을 하는데, 결과의 첫 줄을 보면 맨 앞에 null이라고 출력되어있는 것을 알 수 있다. 참조변수 s는 멤버변수이기 때문에 따로 초기화 해주지 않으면, 자신의 타입에 해당하는 기본값인 null로 초기화 된다. 덧셈연산자(+)는 값이 null인 참조변수를 "null"로 변환한 후 연산을 한다. 그렇기 때문에 "null123456789"와 같은 결과가 나온 것이다.
s2의 경우 빈 문자열("", empty string)로 초기화 했기 때문에 "123456789"와 같은 결과를 얻었다.
이처럼, String형 참조변수를 초기화 하지 않으면 문자열간의 결합에 있어서 문제가 될 수 있으므로, String s ="";와 같이 변수의 선언과 함께 빈 문자열로 초기화 해주는 것이 좋다.
[참고]프로그램을 테스트할 때 에러를 쉽게 발견하기 위한 목적으로 일부러 빈 문자열 대신 null로 초기화 해주기도 한다.





2.2 빈 문자열(empty string)


크기가 0인 배열이 존재할 수 있을까? 답은 '존재할 수 있다.'이다. 빈 문자열이 바로 크기가 0인 char형 배열이다. 전에도 학습했던 것과 같이 문자열은 내부적으로 char배열로 저장된다.

String s = ""과 같이 했을 때, s가 크기가 0인 char형 배열이라고 해서, char c =''와 같은 것은 아니다. char형의 변수에는 반드시 하나의 문자를 지정해야한다.

초기화 하지 않은 char형 멤버변수 c의 경우, '\u0000'로 자동초기화가 이루어진다. '\u0000'은 유니코드의 첫 번째 문자로써 아무런 문자도 지정되지 않은 빈 문자이다. 만일 아무 내용도 없는 빈 문자를 char형 변수에 저장하고자 한다면, char c = '\u0000'과 같이 해야지 char c=''과 같은 표현은 허용되지 않는다.

이처럼 char형 변수의 경우, 변수에 문자를 반드시 지정해 주어야 하지만, char형 배열은 char[] cArray = new char[0];과 같은 표현이 가능하다. 크기가 0이기 때문에 아무런 문자도 저장할 수 없는 배열이라 무의미하게 느껴지겠지만 어쨌든 이러한 표현이 가능하다.
String s ="";과 같은 문장이 있을 때 참조변수 s가 참조하고 있는 String인스턴스의 내부에는 new char[0]과 같이 크기가 0인 char형 배열을 내부적으로 갖고 있는 것이다.
[참고]char형 배열뿐만 아니라 모든 배열이 크기가 0인 배열 생성이 가능하다.


[예제9-12] StringEx6.java

class StringEx6
{
static char[] c = new char[0]; // 크기가 0인 char배열 생성
// 생성자 String(char[] c) 사용
static String s = new String(c); // static String s = new String("");와 같다.
public static void main(String[] args)
{
System.out.println(s);
System.out.println(c.length);
}
}
[실행결과]

0

크기가 0인 배열을 생성해서 char형 배열 참조변수 c를 초기화 해주었다. 크기가 0이긴 해도 배열이 생성되며, 생성된 배열의 주소값이 참조변수 c에 저장된다.
[참고]위 예제결과의 첫 줄에는 빈 문자열("")이 출력된 것이다.





2.2 String클래스의 생성자와 메서드





[참고] 문자 'o'의 코드는 10진수로 111이다.
[참고] replaceFirst, replaceAll, split메서드는 J2SDK1.4에서 새로 추가된 것들이다.


[예제9-13] StringEx7.java

class StringEx7
{
public static void main(String[] args)
{
int value = 100;
String strValue = String.valueOf(value); // int를 String으로 변환한다.

int value2 = 100;
String strValue2 = value2 + ""; // int를 String으로 변환하는 또 다른 방법

System.out.println(strValue);
System.out.println(strValue2);
}
}
[실행결과]
100
100

이 예제는 정수형(int)값을 String으로 변환하는 두 가지 방법을 보여 주고 있다. String클래스의 valueOf메서드는 매개변수로 기본형 변수와 객체를 지정할 수 있으며, 그 결과로 String을 얻을 수 있다.
변수의 값을 String으로 변환하는 또 다른 방법은 덧셈연산자(+)를 사용하는 것이다. 덧셈연산자는 두 개의 피연산자 중 어느 한 쪽이라도 String이면 연산결과는 String이 된다.
그래서 변수에 빈 문자열을 더하면, 그 결과로 String을 얻을 수 있다.
[참고]참조변수에 String을 더하면, 참조변수가 가리키고 있는 인스턴스의 toString()을 호출하여 String을 얻은 다음 결합한다.



[참고] 덧셈연산자(+)는 왼쪽에서 오른쪽으로 연산을 진행해 나가기 때문에 7 + 7 + ""에서는 7 + 7을 먼저 수행한 다음에 그 결과인 14와 ""를 더해서 "14"가 된다.


지금까지 기본형 값을 문자열로 바꾸는 방법에 대해서 알아봤으니, 이제는 문자열을 기본형 값으로 바꾸는 방법에 대해서 정리해 보자.


[참고] byte, short값을 문자열로 변경할 때는 String valueOf(int i)를 사용하면 된다.


위의 표에 있는 메서드만 알고 있으면, 문자열과 기본형 값의 변환에는 아무런 문제가 없을 것이다. 이 변환은 프로그래밍에서 반드시 알고 있어야 하는 아주 중요한 내용이다.

[예제9-14] StringEx8.java

class StringEx8
{
public static void main(String[] args)
{
String[] numbers = { "1", "2", "3", "4", "5" };
String result1 = "";
int result2 = 0;

for(int i=0; i < numbers.length; i++) {
result1 += numbers[i];
result2 += Integer.parseInt(numbers[i]);
}

System.out.println("result1 : " + result1);
System.out.println("result2 : " + result2);
}
}
[실행결과]
result1 : 12345
result2 : 15

이 예제는 문자열을 정수형(int) 값으로 변환하는 예를 보여 준 것이다. 문자열에 공백 또는 문자가 포함되어 있는 경우 변환 시 예외(NumberFormatException)가 발생할 수 있으므로 주의해야한다.
그러나 소수점을 의미하는 '.' 이나 float형 값을 뜻하는 f와 같은 자료형 접미사는 허용된다. 단, 자료형에 알맞은 변환을 하는 경우에만 허용된다.
만일 "1.0f"를 int형 변환 메서드인 Integer.parseInt(String s)을 사용해서 변환하려하면 예외가 발생된다. 이때는 Float.parseInt(String s)를 사용해야한다.
이처럼 문자열을 숫자로 변환하는 과정에서는 예외가 발생하기 쉽기 때문에 주의를 기울여야 한다.

[알아두면 좋아요.]
Integer클래스의 static int parseInt(String s, int radix) 를 사용하면 16진수 값으로 표현된 문자열도 변환할 수 있기 때문에 대소문자 구별없이 a, b, c, d, e, f도 사용할 수 있다.
int result = Integer.parseInt("a", 16);의 경우 result에는 정수값 10이 저장된다.(16진수 a는 10진수로는 10을 뜻한다.)

[예제9-15] StringEx9.java

class StringEx9
{
public static void main(String[] args)
{
String fullName = "Hello.java";

// fullName에서 '.'의 위치를 찾는다.
int index = fullName.indexOf('.');

// fullName의 첫번째 글자부터 '.'이 있는 곳까지 문자열을 추출한다.
String fileName = fullName.substring(0, index);

// '.'의 다음 문자 부터 시작해서 문자열의 끝까지 추출한다.
// fullName.substring(index+1, fullName.length());의 결과와 같다.
String ext = fullName.substring(index+1);

System.out.println(fullName + "의 확장자를 제외한 이름은 " + fileName);
System.out.println(fullName + "의 확장자는 " + ext);
}
}
[실행결과]
Hello.java의 확장자를 제외한 이름은 Hello
Hello.java의 확장자는 java

위 예제는 substring메서드를 이용하여 한 문자열에서 내용의 일부를 추출하는 예를 보인 것이다. substring(int start, int end)를 사용할 때 주의해야할 점은 매개변수로 사용되는 문자열에서 각 문자의 위치를 뜻하는 index가 0부터 시작한다는 것과 start부터 end의 범위 중 end위치에 있는 문자는 결과에 포함되지 않는다는 것이다.
[참고] end에서 start값을 빼면 substring에 의해 추출될 글자의 수가 된다.



[참고] substring의 철자에 주의하도록 한다. subString이 아니다.


[예제9-16] StringCount.java

public class StringCount
{
private int count;
private String source ="";

public StringCount(String source) {
this.source = source;
}

public int stringCount(String s) {
return stringCount(s, 0);
}

public int stringCount(String s, int pos) {
int index = 0;
if (s == null || s.length() == 0)
return 0;
if ( (index = source.indexOf(s, pos))!=-1) {
count++;
stringCount(s, index +s.length());
}
return count;
}

public static void main(String[] args)
{
String str = "aabbccAABBCCaa";
System.out.println(str);
StringCount sc = new StringCount(str);
System.out.println("aa를 " + sc.stringCount("aa") +"개 찾았습니다.");
}
}
[실행결과]
aabbccAABBCCaa
aa를 2개 찾았습니다.

하나의 긴 문자열(source) 중에서 특정 문자열과 일치하는 문자열의 개수를 구하는 예제이다. stringCount메서드는 재귀호출을 이용해서 반복적으로 일치하는 문자열의 개수를 구한다.
이 예제를 응용해서 문자열을 치환하는 예제를 한번 작성해보도록 하자.

 

반응형