String

고동현·2024년 7월 5일
0

JAVA

목록 보기
13/22

String은 클래스이다.

String은 int,boolean과 같은 기본형이 아닌 참조형이다.

String 클래스에 가보면 실제 문자를 저장할수있는 value 배열이 존재함을 알 수 있다.
이러한 필드를 바탕으로 메서드를 String 클래스에서 제공한다.

참고: 자바 9이전에는 byte가 아니라 char였는데, 문자는 2byte가 필요하지만, 영어나 숫자로 이루어진 문자는 1byte로 표현이 가능하므로 byte로 변경되었다.

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("result2 = " + result2);
        System.out.println("result1 = " + result1);
    }
}

String a = "hello"
원래는 new String("hello")해야하는데 자바가 봐준다.
당연히 class이므로 .메서드 호출이 가능하다.
a+b도 원래는 참조값이니까 x001+x002라서 안되는건데 자바가 편의를 봐준다.

동등성 비교

  • ==: 동일성 두 객체의 참조가 동일한 객체를 가르키고 있는지 확인
  • 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("equals비교: "+(str1.equals(str2)));

        String str3 = "hello";
        String str4 = "hello";
        System.out.println("==비교: "+(str3==str4));
        System.out.println("equals비교: "+(str3.equals(str4)));

    }
}

결과가 어떻게 나올까? 예상을 해보자

==비교: false
equals비교: true
==비교: true
equals비교: true

str1과 str2에는 서로 다른 인스턴스가 만들어지니까 ==비교가 false, equals는 true인건 알겠다.

분명 str3도 원래는 new String("hello")로 자바가 만들어줘서 서로 x003,x004로 다를텐데 어째서 동일성이 통과하는 것일까?

바로 문자열 풀에 답이 있다.

String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바가 실행되는 시점에 문자열 풀에 String 인스턴스를 미리 만들어 둔다.
그러므로 String str4와 같은 경우 "hello"문자열 리터럴을 사용하므로 문자열 풀에서 이미 있는 x003을 참조해서 사용한다.

public static boolean isSame(String x, String y){
  return x==y?
  return x.equals(y)?
}

만약에 이런 문자열 비교 함수를 만든다면 어떻게 해야할까?
반드시 equals()메서드를 사용해야한다.
파라미터로 넘어오는 x와y가 리터럴로 만들어 졌는지 new로 만들어 졌는지 알수가 없다.
그러므로, 항상 동등성을 판단하여서 결과를 반환해야한다.

String은 불변이다.

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

이렇게하면 뭔가 hello java로 나올것 같지만,

보면 value type이 final이다. 고로 hello가 할당 된 다음에 절대로 바꿀 수 가 없다.

그러므로, 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환해야한다.

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

그렇다면 String이 왜 불변으로 설계되었을까?
문자열 풀에서 생각해보면 알 수 있다.

리터럴로 str3와 str4가 만들어졌다면, 현재 x003으로 동일한 인스턴스를 참조하고있다.
String str3 = "hello";
String str4 = "hello";

그런데 여기서 str3이 hello를 hhh로 바꿔버리면, 동일하게 x003을 참조하고 있던 str4도 hhh로 바뀌어 버리는 사이드 이펙트가 발생한다.
고로 String은 이러한 경우를 방지하기 위해 불변으로 만들었다.

가변 String

String str = new String("AB")+new String("ABC")+new String("ddd");
라고 치면 결국 String은 불변이므로 new String("ABABCddd")객체를 반환하므로
나머지 AB,ABC,ddd 객체는 쓰이지 않게된다.
str은 ABABCddd객체만 참조하게 된다.
나머지 3개의 인스턴스는 GC가 버리게 된다.

그렇다면, 그냥 private이 아니고 가변으로 설정할수있는 class는 없을까?
StringBuilder이다.


StringBuilder는 byte가 final이아니라 가변적이다.

public class StringBuilderMain1_1 {
    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("sb = " + sb);

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

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

이처럼 StringBuilder는 문자열을 변경이 가능하고(해당 문자열을 변경할때마다, 새로운 객체를 생성하지 않는다.), 해당 문자열을 toString메서드를 통해 String class로 변환도 가능하다.

그래서, StringBuilder는 보통 문자열을 변경하는 동안만 사용되다가, 문자열 변경이 끝나면 안전한 String으로 변환한다.

String 최적화

문자열 리터럴 최적화
String helloWorld = "Hello, " + "World!";
라고 하면, Hello,와 Worlld! String 객체를 문자열 풀에 만드는게 아니라, 자바가 그냥 Hello, World! String 인스턴스를 문자열 풀에 만들어준다.

String result = str1 + str2;
자바가 내부에서 new StringBuilder().append(str1).append(str2).toString()
이런식으로 최적화를 알아서 처리해준다.(str1,str2인스턴스를 만들고 새로운 str1+str2인스턴스를 만들어서 대입후 str1,str2는 GC가 지우고 이런과정 x)

그러나 최적화가 어려운경우가 있다.

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(endTime-startTime+"ms");
    }
}

문자열을 루프안에서 더하는 경우에는 최적화가 어렵다.
왜냐하면 반복문 내에서
result = new StringBuilder().append(result).append(hello java).toString
이므로 반복 횟수만큼 객체를 생성해야한다. 이런상황에서는 최적화가 어렵다.
총 100000개의 String 객체를 생성했을것이다.

이럴때는 직접 StringBuilder를 사용하면되다. 이러면 반복문 횟수만큼 객체를 만드는게아니라 StringBuilder에다가 가변적으로 문자를 이어붙이므로 효율적이다.

public class LoopStringBuilderMain {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        StringBuilder result =new StringBuilder();
        for(int i=0;i<100000;i++){
            result.append("hello java");
        }
        long endTime = System.currentTimeMillis();

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

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

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

StringBuilder vs StringBuffer
StringBuilder와 동일한 역할을 수행하는 StringBuffer가 있다.
StringBuffer는 내부에 동기화가 되어있어서, 멀티 스레드 환경에서 안전하지만, 동기화 오버헤드로 성능이 느리다.
그래서 String Builder는 멀티 쓰레드 상황에서 안전하지 않지만, 동기화 오버헤드가 없으므로 속도가 빠르다.

StringBuffer는 메서드에서 synchronized 키워드 사용
sychronized는 현재 데이터 사용하고 있는 스레드를 제외하고 나머지 스레드들이 데이터에 접근할 수 없도록 막는다.

  • A스레드: sb의 append()동기화 블록에 접근 및 실행
  • B스레드: A스레드 sb의 append()동기화 블록에 들어가지 못하고 block
  • A스레드: sb의 append()동기화 블록에서 탈출
  • B스레드: block에서 running상태로 변경 sb의 append()동기화 블록에 접근 및 실행

메서드 체이닝

간단한 코드를 통해 메서드 체이닝에 대해서 알아보자

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

    public int getValue() {
        return value;
    }
}

add메서드의 반환형이 참조형인것을 알 수 있다.
add메서드에서 로직을 수행한후 자신의 참조값을 반환하고 있다.

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

adder에 0x001이 들어있으면
0x001.add(1) -> 0x001.add(2) -> 0x001.add(3) 이런식으로 자신의 참조값을 반환하여 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있다.

StringBuilder도 이런식으로 구현을 해두었다.

public class MethodChainingMain3 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        String str = sb.append("A").append("B").append("C")
                .insert(3,"java")
                .reverse()
                .toString();
        System.out.println(str);
    }
}

sb에는 StringBuilder()생성자를 통한 인스턴스가 들어가고,
sb.append("A")를하면 해당 인스턴스의 참조값을 반환하므로 0x001.append("B")가 된다.

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글