Java

String 클래스

공부처음하는사람 2024. 10. 15. 01:07

 

String은 클래스이다. 즉 참조형이다

 

문자열은 자주 사용되니 편의상 쌍따옴표로 문자열을 감싸면 자바에서 아래와 같이 자동으로 변경해준다.

 

package lang.string;

public class StringBasicMain {

    public static void main(String[] args) {
        String str1 = "hello";
        //String str1 = new String("hello"); 
    }
}

(실제로는 성능 최적화를 위해 문자열 풀을 사용한다)

 

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {

    @Stable
    private final byte[] value;
    private final byte coder;
	...
    
    public String concat(String str) {...}
    public int length() {...}

 

자바 클래스의 구조이다. java 9 이전버전은 byte가 아닌 char[] value;로 구성되어있다.

char는 2byte를 사용하는데, latin_1 인코딩의 경우 영어와 숫자는 1바이트로 사용가능하기 때문에 메모리를 효율적으로

사용하기 위해 char -> byte로 바뀌었다고 함. (UTF-16 인코딩은 2바이트)

 

String의 메서드는 다양한 기능을 제공한다. 필요한게 있을 땐 API 문서를 찾아보자.

length : 문자열 길이반환

charAt : 특정 인덱스 반환

substring : 문자열 부분 반환

indexOf : 특정 문자열 시작되는 인덱스 반환

toLowerCase, toUpperCase : 문자열을 소문자 또는 대문자 변환

trim : 문자열 양 끝 공백 제거

concat : 문자열 더하기 

 

*String은 참조형이기 때문에 원칙적으로는 +로 문자열을 더할 수 없다. 하지만 자바에선 문자열은 자주 다루기 때문에

편의상 +로 문자열을 더하는것을 허용한다. (concat)

 

String 클래스 - 비교

동일성(Identity) : == 연산자를 이용해 참조가 동일한 객체를 가리키고 있는지 확인

동등성(Equality) : .equals() 를 사용해 두 객체가 논리적으로 같은지 확인

 

package lang.string.equals;

public class StringEqualsMain1 {

    public static void main(String[] args) {
        String str1 = new String("hello");
        String str2 = new String("hello");
        System.out.println(str1 == str2);
        System.out.println(str1.equals(str2));

        // false, true

        String str3 = "hello";
        String str4 = "hello";
        System.out.println(str3 == str4);
        System.out.println(str3.equals(str4));
        
        // true, true
    }
}

 

str1 == str2의 결과값은 false다. 이유는 str1과 str2는 각각의 인스턴스를 생성했기 때문에 동일성 비교에서 실패한 것

str1.equals(str2)는 내부에 같은 값을 가지고 있기 때문에 동등성 비교에 성공했다.

 

문자열 풀

 

아래 str3 == str4 결과는 true이다. 그 이유는 문자열 풀 때문인데, 같은 문자열 리터럴을 사용하는 경우에 자바는

메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.

자바가 실행되는 시점에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어두는데, 이 때

같은 문자열이 있다면 추가로 만들지 않는다.

 

풀(Pool)은 공용자원을 모아두는 곳을 뜻한다. 여러곳에서 사용할 수 있는 자원을 필요할때마다 생성하고 제거하는것은

비효율적이기에 재사용하기 위해 풀이 사용된다. (문자열 풀은 힙 영역을 사용한다)

 

어차피 문자열 풀을 사용하니까 문자열 비교를 할 땐 ==를 사용하면 되지 않을까 라고 생각할 수 있지만

문자열은 equals 메서드를 사용해서 비교해야한다. 그 이유는 문자열을 비교하는 메서드가 있다고 가정했을 때,

파라미터로 넘어오는 인스턴스가 문자열 리터럴을 비교할 지, new String으로 생성된 문자열을 비교할 지 모르기 때문이다.

 

String 클래스 - 불변 객체

String은 불변 객체이다. 따라서 내부의 문자열 값을 변경할 수 없다.

 

package lang.string.immutable;

public class StringImmutable1 {

    public static void main(String[] args) {
        String str = "hello";
        str.concat("java");
        System.out.println(str);

    }
}

 

concat 메서드를 사용해 문자열을 연결하려고 했다. 그러나 출력 결과는 hello만 나오게 되는데, 그 이유는 String은 불변객체이기 

때문이다. 따라서 hello java 를 출력하려면 기존 값을 변경하지 않고 새로운 결과를 만들어서 반환해야한다.

 

package lang.string.immutable;

public class StringImmutable2 {

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

 

String.concat은 내부에서 새로운 String 객체를 만들어서 반환한다. 따라서 기존 객체 값은 유지하는 것이다

String은 자바 내부에서 문자열 풀을 통해 최적화하는데, 만약 String 내부의 값을 변경할 수 있게 된다면 문자열 풀에서

같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생하니 String은 불변 객체가 되야하는 것이다.

 

String 클래스의 메서드는 아주 많은 메서드가 있으니 필요할 때 검색해서 사용하면 된다. (일일이 다 외우기 힘들다)

 

StringBuilder - 가변 String

 

불변인 String도 단점이 있다. String은 불변이므로 내부의 값을 변경할 수 없기 때문에, 변경 된 값을 기반으로 새로운 String 객체를 

생성한다.

 

tring 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 ("A"), new String ("AB"), new String ("ABC"), new String ("ABCD") 총 4개의 객체가 생성된다.

우리는 최종적으로 "ABCD"의 값을 가진 객체가 필요한 것인데, 그 과정에서 필요 없는 3개의 객체가 생성되고, GC를 통해 삭제된다.

결과적으로 CPU, 메모리 자원을 더 많이 사용하게 되기 때문에 굉장히 비효율적인 코드이다.

(실제로는 자바 내부에서 최적화를 하긴 한다)

 

이때 사용되는것이 StringBuilder 이다.

StringBuilder는 가변객체이다. 따라서 객체의 값을 변경할 때, 객체를 생성하지 않고 내부에서 바로 변경할 수 있게 된다.

 

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    byte[] value;

 

StringBuilder는 final이 아닌 변경할 수 있는 byte[]를 가지고 있는 것을 확인할 수 있다. StringBuilder는 보통 문자열을 변경하는

동안에만 사용하다가, 문자열 변경이 끝나게 되면 안전한(불변) String으로 변환하는 것이 좋다. (사이트이펙트가 발생할 가능성이 있기때문이다)

String string = sb.toString();
System.out.println("String" + string);

 

 

String 최적화

자바 컴파일러는 문자열 리터럴을 더하는 부분을 자동적으로 합쳐준다.

String helloWorld = "Hello, " + "World!";
//컴파일 후
String helloWorld = "Hello, World!";

 

문자열 변수의 경우는 컴파일 시점에 변수에 어떤값이 들어가있는지 알 수 없기때문에 단순하게 합칠 수 없다.

String result = str1 + str2;
//최적화 (자바의 버전에 따라 최적화 방법은 다를 수 있음)
String result = new StringBuilder().append(str1).append(str2).toString();

 

지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 충분하다. 다만 최적화가 어려운 경우가 있는데,

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

 

package lang.string.builder;

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");
    }
}

 

위 반목문의 경우 어떤 방식으로 최적화가 되냐면

String result = "";
for (int i = 0; i < 100000; i++) {
	result = new StringBuilder().append(result).append("Hello Java").toString();
}

 

최적화가 되있는 것 처럼 보이지만, 반복 횟수만큼 객체가 생성되어야 한다. 반복문 내에서의 문자열 연결은 런타임에 연결할 문자열의 개수

와 내용이 결정되기 때문에, 컴파일러는 얼마나 많은 반복이 일어날지 어떻게 변할 지 예측할 수 없기 때문에 최적화가 어려운 것이다.

위 코드는 아마 10만번의 String객체가 생성되고, GC로 객체가 삭제되는 행위가 일어났을 것이다.

 

이럴 경우엔 StringBuilder를 사용하면 된다.

 

package lang.string.builder;

public class LoopStringMain {

    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 ");
        }
        Long endTime = System.currentTimeMillis();

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

 

StringBuilder를 사용하고 안하고의 차이는 약 800배정도 였다. 

 

StringBuilder를 직접 사용하면 좋은 경우

1. 반복문에서 반복해서 문자를 연결할 때

2. 조건문을 통해 동적으로 문자열을 조합할 때

3. 복잡한 문자열의 특정 부분을 변경해야 할 때

4. 매우 긴 대용량 문자열을 다룰 때

 

StringBuilder와 StringBuffer는 똑같은 기능을 수행하는데, 이 차이는 나중에 따로 검색을 해볼 것

 

메서드 체이닝 - Method Chaining

package lang.string.chaining;

public class ValueAdder {

    private int value;

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

    public int getValue() {
        return value;
    }
}

 

add() 메서드를 호출할 때마다 내부의 value의 값을 누적하는 코드이다. add()메서드의 반환값은 this를 반환한다.

 

package lang.string.chaining;

import com.sun.jdi.Value;

public class MethodChainingMain1 {

    public static void main(String[] args) {
        ValueAdder adder = new ValueAdder();
        adder.add(1);
        adder.add(2);
        adder.add(3);

        int result = adder.getValue();
        System.out.println(result);
    }
}

 

위 코드는 add메서드의 반환값을 사용하지 않은 코드이다. 반환된 값을 사용하지 않고 넘어간 것

 

 

package lang.string.chaining;

public class MethodChainingMain2 {

    public static void main(String[] args) {
        ValueAdder adder = new ValueAdder();
        ValueAdder adder1 = adder.add(1);
        ValueAdder adder2 = adder1.add(2);
        ValueAdder adder3 = adder2.add(3);


        int result = adder3.getValue();
        System.out.println(result);
    }
}

 

이 코드는 add메서드의 반환값을 사용한 코드이다.

adder.add(1)을 호출하고, add() 는 결과를 누적하고 자신의 참조값인 this (ValueAdd 인스턴스)를 반환

adder1은 adder와 같은 인스턴스를 참조한다.

 

add() 메서드는 자기 자신의 참조값을 반환한다. 이 반환값을 adder1, adder2, adder3에 보관했다.

따라서 adder1,2,3은 모두 같은값을 참조한다. add()메서드가 자기 자신의 참조값을 반환하기 때문이다.

 

그런데 이 코드는 사용하기도 불편하고, 잘 읽히지도 않는다. 이 문제를 해결하기 위해 메서드체이닝을 사용해보면

 

package lang.string.chaining;

public class MethodChainingMain3 {

    public static void main(String[] args) {
        ValueAdder adder = new ValueAdder();
        int result = adder.add(1).add(2).add(3).getValue();

        System.out.println(result);
    }
}

 

add() 메서드를 호출하면 자신의 참조값이 반환된다. 이 참조값을 변수에 담지 않고 연속적으로 참조해서 바로 호출할 수 있다.

위 방식처럼 . 을 찍고 계속 연결해서 사용한다. 메서드가 체인으로 연결된 것 처럼 보인다. 이러한 기법을 메서드 체이닝이라고 한다.

위 방식이 사용 가능한 이유는 자기 자신의 참조값을 반환하기 때문에 변수값을 생략하고 호출할 수 있는것이다.

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

'Java' 카테고리의 다른 글

열거형 - ENUM  (2) 2024.11.05
래퍼 클래스, Class 클래스  (3) 2024.10.17
불변 객체  (0) 2024.10.10
Object  (1) 2024.10.09
제네릭 - 제한된 타입 파라미터/ 와일드카드  (0) 2024.07.11