본문 바로가기

Programming/Java

[Java] 김영한의 자바 중급 1편 #3 - String 클래스

반응형

 

 

김영한 선생님의 자바 중급을 듣고 정리한 내용입니다.

 

 


1. String 클래스

자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있다.

 

package stringClass;

public class CharArrayMain {
    public static void main(String[] args) {
        char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'};
        System.out.println(charArr);

        String str = "hello";
        System.out.println("str = " + str);
    }
}

# 실행 결과
hello
str = hello

 

 

하지만 이렇게 char[]을 직접 다루는 방법은 매우 불편하기 때문에 자바에서는 문자열을 편리하게 다룰 수 있도록 String 클래스를 제공한다.

String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있다.

[1] 쌍따옴표 사용하기 “hello”
→ String은 참조형 클래스이기 때문에 String str = “hello” 처럼 사용하는 것은 조금 어색할 수 있다. 하지만 매우 자주 사용되기 때문에 편의상 쌍따옴표로 감싸서 사용할 수 있도록 자바에서 허용해준다.

[2] 객체 생성 new String(”hello”)

 

 

1.1. String 클래스 구조

String 클래스는 대략 다음과 같이 생겼다.

 

public final class String {
	
	// java 9 이전
	private final char[] value;
	
	// java 9 이후
	private final byte[] value;
}

 

  • String 실제 문자열 값이 위 Array 안에 보관된다. 즉, 문자 데이터 자체는 char[] 혹은 byte[]에 보관된다는 것이다.
  • 문자 하나에 char는 2byte를 차지한다. 보통 영어, 숫자는 1 byte로 표현이 가능하기 때문에 단순히 영어, 숫자로 표현된 경우 1 byte를 사용하고 그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었다.

 

 

1.2. String 클래스와 참조형

String 클래스는 참조형이다. 따라서 참조형은 객체를 생성하면 x001과 같이 참조값이 들어있다.

→ 따라서 원칙적으로 +와 같은 연산을 사용할 수 없다.

→ 하지만 문자열을 너무 자주 다루어지기 때문에 자바 언어에서 특별히 + 연산을 제공한다.

 

package lang.string;
public class StringConcatMain {
	public static void main(String[] args) {
	String a = "hello";
	String b = " java";
	String result1 = a.concat(b);
	String result2 = a + b;
	System.out.println("result1 = " + result1);
	System.out.println("result2 = " + result2);
	}
}

// 결과
result1 = hello java
result2 = hello java

 

 

 

1.3. String 클래스 - 비교

String 클래스 비교할 때는 항상 == 연산자가 아니라 equals() 메서드를 활용해야 한다.

 

package stringClass;

public class StringEqualsMain {

    public static void main(String[] args) {

        String str1 = new String("hello");
        String str2 = new String("hello");

        System.out.println("new String() == 연산자 : " + (str1 == str2));
        System.out.println("new String() equals 연산자 : " + (str1.equals(str2)));

        System.out.println("==========================================");

        String str3 = "hello";
        String str4 = "hello";

        System.out.println("new String() == 연산자 : " + (str3 == str4));
        System.out.println("new String() equals 연산자 : " + (str3.equals(str4)));
    }
}

// 결과
new String() == 연산자 : false
new String() equals 연산자 : true
==========================================
new String() == 연산자 : true
new String() equals 연산자 : true

Process finished with exit code 0

 

 

 

1.3.1. new String()으로 만든 인스턴스

  • str1, str2 == 연산자 : new String()을 사용해서 각각 인스턴스를 생성했다. 서로 다른 인스턴스이므로 동일성(==) 비교에 실패한다.
  • str1, str2 equals 메서드 : “hello” 값을 가지고 있기 때문에 논리적으로 같다. 따라서 동등성(equals()) 비교에 성공한다. 참고로 String 클래스는 내부 문자열 값을 비교하도록 equals() 메서드를 재정의 해두었다.

 

 

 

1.3.2. String xx = “yy” 로 만든 인스턴스

문자열 리터럴을 통해 문자열 객체를 생성하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.

  • 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둔다. → 이때 같은 문자열이 있으면 만들지 않는다.
  • String str3 = “hello” 와 같이 문자열 리터럴을 사용하면 문자열 풀에서 “hello”라는 문자를 가진 String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조(x003)을 반환한다.
  • String str4 = “hello”의 경우 문자열 리터럴을 동일하게 사용하는데, 이 때 hello라는 문자를 가진 String 인스턴스가 이미 있기 때문에 str3와 같은 x003 참조를 같이 사용한다.

따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공하게 된다.

 

 

 

 

2. String 클래스 - 불변 객체

String은 불변 객체이다. 따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없다.

 

package stringClass;

public class StringImmutable {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = str1.concat(" java");
        System.out.println("str1 = " + str1);
        System.out.println("str2 = " + str2);
    }
}

// 결과
str1 = hello
str2 = hello java

 

 

 

  • String.concat()은 내부에서 새로운 String 객체를 만들어서 반환한다.
    → 따라서 불변과 기존 객체의 값을 유지한다.

 

3. String 클래스의 주요 메서드

  • 문자열 비교
    • equals(Object anObject)
    • equalsIgnoreCase(String anotherString)
    • compareTo(String anotherString)
    • compareToIgnoreCase(String str)
    • startsWith(String prefix)
    • endsWith(String suffix)
  • 문자열 검색
    • contains(CharSequence s)
    • indexOf(String ch)
    • indexOf(String ch, int fromIndex)
    • lastIndexOf(String ch)
  • 문자열 조작 및 변환
    • substring(int beginIndex)
    • substring(int beginIndex, int endIndex)
    • concat(String str)
    • replaceAll(String regex, String replacement)
    • replaceFirst(String regex, String replacement)
    • toLowerCase()
    • toUpperCase()
    • trim()
    • strip()
  • 문자열 분할 및 조합
    • split(String regex)
    • join(CharSequence delimiter, CharSequence… elements)
  • 기타 유틸리티
    • valueOf(Object obj)
    • toCharArray()
    • format(String format, Object… args)
    • matches(String regex)
  • 문자열 정보 조회
    • length()
    • isEmpty()
    • isBlank()
    • charAt(int index)

 

4. StringBuilder - 가변 String

4.1. 불변인 String 클래스 단점

불변인 String 클래스에도 단점이 있는데, 다음의 예시를 살펴본다.

→ 불변인 String 내부의 값은 변경할 수 없고 변경된 값을 기반으로 새로운 String 클래스를 생성한다.

 

String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D"); 
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");

 

 

중간에 만들어진 new String(”AB”), new String(”ABC”)는 사용되지 않으며 new String(”ABCD”)만 사용된다.
→ 중간에 만들어진 AB, ABC는 제대로 사용되지도 않고 추후 GC 대상이 될 뿐이다.

즉, 불변인 String 클래스의 단점은 문자열을 더하거나 변경할 때마다 계속해서 새로운 객체를 생성해야 한다는 점이다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고 GC 해야만 한다.

결과적으로 컴퓨터의 CPU, Memory 자원을 더 많이 소모하게 된다.

 

 

4.2. StringBuilder

이런 단점을 해결하기 위해 StringBuilder를 사용할 수 있다. 자바에서는 StringBuilder이라는 가변 String을 제공한다. (물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 한다.)

 

// StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]를 가지고 있다.
public final class StringBuilder {
	char[] value;// 자바 9 이전
	byte[] value;// 자바 9 이후
	
  //여러 메서드
  public StringBuilder append(String str) {...}
  public int length() {...}
  ...
}

 

 

 

package stringClass;

public class StringBuilderMain {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();

        sb.append("A");
        sb.append("B");
        sb.append("C");
        sb.append("D");
        System.out.println("sb = " + sb);

        sb.insert(4, "Java");
        System.out.println("insert = " + sb);

        sb.delete(4, 8);
        System.out.println("delete = " + sb);

        sb.reverse();
        System.out.println("reverse = " + sb);

        //StringBuilder -> String
        String string = sb.toString();
        System.out.println("string = " + string);
    }
}

// 결과
sb = ABCD
insert = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA

 

 

  • StringBuilder 객체를 생성한다.
  • append() 메서드를 사용해 여러 문자열을 추가한다.
  • insert() 메서드로 특정 위치에 문자열을 삽입한다.
  • delete () 메서드로 특정 범위의 문자열을 삭제한다.
  • reverse() 메서드로 문자열을 뒤집는다.
  • 마지막으로 toString 메소드를 사용해 StringBuilder 의 결과를 기반으로 String 을 생성해서 반환한다.

 

 

4.3. String 최적화

4.3.1. String 최적화가 어려운 케이스

문자열을 루프 안에서 문자열을 더하는 경우 최적화가 잘 이루어지지 않는다.

 

package stringClass;

public class LoopStringMain {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        String result = "";
        for (int i = 0; i < 100000; i++) {
            result += "Hello Java ";
        }
        long endTime = System.currentTimeMillis();

        System.out.println("result = " + result);
        System.out.println("time = " + (endTime - startTime) + "ms");
    }
}

// 결과
... Java Hello Java Hello Java Hello Java ...
time = 3920ms

 

 

  • 반복문 내에서의 문자열 연결은 런타임에 연결할 문자열의 개수와 내용이 결정된다.
  • 이런 경우 컴파일러는 얼마나 많은 반복이 일어날지 예측할 수 없다. 따라서 이런 상황에서는 최적화가 잘 이루어지지 않는다.

 

4.3.2. 이럴 때, StringBuilder를 사용하면 된다.

package stringClass;

public class LoopStringBuilderMain {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            sb.append("Hello Java ");
        }
        String result = sb.toString();
        long endTime = System.currentTimeMillis();
        System.out.println("result = " + result);
        System.out.println("time = " + (endTime - startTime) + "ms");
    }
}

// 결과
Java Hello ... Hello Java 
time = 6ms

 

 

StringBuilder를 직접 사용하는 것이 더 좋은 이유

  • 반복문에서 반복해서 문자를 연결할 때
  • 조건문을 통해 동적으로 문자열을 조합할 때
  • 복잡한 문자열의 특정 부분을 변경해야 할 때
  • 매우 긴 대용량 문자열을 다룰 때

 

4.4. StringBuilder vs StringBuffer

StringBuilder : 내부에 동기화가 되어 있어서 멀티 스레드 상황에서 안전하지만 동기화 오버헤드로 인해 성능이 느리다. (StringBuffer와 동기화에 관한 내용은 이후 멀티스레드를 학습해야 이해할 수 있음)

StringBuilder : 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠르다.

 

 

 

5. 메서드 체이닝

간단한 예제 코드로 메서드 체이닝에 대해 알아본다.

ValueAdder 클래스는 단순히 값을 누적해서 더하는 기능을 제공하는 클래스이다.

  • add() 메서드를 호출할 때 마다 내부의 value에 값을 누적한다.
  • add() 메서드를 보면 자기 자신(this)의 참조값을 반환한다.
package stringClass;

public class ValueAdder {
    private int value;
    public ValueAdder add(int addValue) {
        value += addValue;
        return this;
    }
    public int getValue() {
        return value;
    }
}

 

 

아래는 ValueAdder 클래스의 add 메서드에서 반환된 참조값(this)를 바로 메서드 호출에 사용하는 예시이다.

package stringClass;

public class MethodChainingMain {
    public static void main(String[] args) {
        ValueAdder adder = new ValueAdder();
        int result = adder.add(1).add(2).add(3).getValue();
        System.out.println("result = " + result);
    }
}

// 결과
result = 6

Process finished with exit code 0

 

 

→ add() 메서드를 호출하면 ValueAdder 인스턴스의 참조값(x001)이 반환된다. 이 반환된 참조값을 변수에 담아두지 않아도 되며 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있게 된다.

 

→ .을 찍고 메서드를 계속 연결해서 사용한다. 마치 메서드가 체인으로 연결된 것처럼 보인다. 이렇나 기법을 메서드 체이닝이라고 한다.

 

→ 메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어준다.

 

 

5.1. 메서드 체이닝와 StringBuilder

StringBuilder는 메서드 체이닝 기법을 제공한다.

 

// StringBuilder의 append() 메서드는 return this에서 볼 수 있듯이
// 자기 자신의 참조값을 반환한다.
public StringBuilder append(String str) {
  super.append(str);
  return this;
}

 

 

StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환한다. (ex. insert(), delete(), reverse() )

 

package stringClass;

public class StringBuilderMethodChainingMain {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        String string = sb.append("A").append("B").append("C").append("D")
                .insert(4, "Java")
                .delete(4, 8)
                .reverse()
                .toString();
        System.out.println("string = " + string);
    }
}

// 결과
string = DCBA

 

반응형