String 클래스

황상익·2024년 5월 7일

Inflearn JAVA

목록 보기
26/61

String 클래스 - 기본

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

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

        String str = "Hello";
        System.out.println("str = " + str);
    }
}
public class StringBasicMain {
    public static void main(String[] args) {
        String str = " Hello";
        String str2 = new String("Hello");

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

String은 클래스다. int, boolean 같은 기본형이 아니라 참조형이다.
따라서 str 변수에는 String 인스턴스 참조값만 들어갈 수 있다.

문자열은 자주 사용, 따라서 편의상 쌍따움표로 문자열을 감싸면 자바 언어에서 new String("Hello")와 같이 변경

String str1 = "hello"; //기존
String str1 = new String("hello"); //변경

String 클래스 구조

public final class String {
 //문자열 보관
 private final char[] value;// 자바 9 이전
 private final byte[] value;// 자바 9 이후
 //여러 메서드
 public String concat(String str) {...}
 public int length() {...}
}

속성
private final char[] value;
String의 실제 문자열 값이 보관, 문자 데이터 자체는 char[]에 보관
String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공

참고)
자바 9 이후 String 클래스 변경 사항
char[] 대신 byte[] 사용

기능
String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공

length() : 문자열의 길이를 반환한다.
charAt(int index) : 특정 인덱스의 문자를 반환한다.
substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환한다.
indexOf(String str) : 특정 문자열이 시작되는 인덱스를 반환한다.
toLowerCase() , toUpperCase() : 문자열을 소문자 또는 대문자로 변환한다.
trim() : 문자열 양 끝의 공백을 제거한다.
concat(String str) : 문자열을 더한다.

String 클래스와 참조형

String은 클래스, 기본형이 아니라 참조형
참조형 변수에 계산할 수 있는 값이 들어있는 것이 아니라 계산할 수 없는 참조값이 들어있다.
따라서 원칙적으로는 +와 같은 연산 사용 X

public class StringConcatMain {
    public static void main(String[] args) {
        String a = "Hello";
        String b = "Java";

        String rst1 = a.concat(b);
        String rst2 = a + b;

        System.out.println("rst1 = " + rst1);
        System.out.println("rst2 = " + rst2);
    }
}

자바에서 문자열을 더할때는 concat 같은 메서드 사용
하지만 문자열을 자주 다루기 때문에 + 연산 특별히 허용

String 클래스 비교

String 클래스를 비교 할때는 ==이 아닌 equals 비교

동일성 -> == (객체의 참조가 동일한지)
동등성 -> equals (논리적으로 동일한지)

public class StringEqualsMain1 {
    public static void main(String[] args) {
        String str1 = new String("Hello");
        String str2 = new String("Hello");

        System.out.println("equals 비교 = " + (str1.equals(str2)));
        System.out.println("== 비교 = " + (str1 == str2));


        System.out.println("리터럴 비교");
        String str3 = "Hello";
        String str4 = "Hello";

        System.out.println("equals 비교 = " + (str3.equals(str4)));
        System.out.println("== 비교 = " + (str3 == str4));
    }
}


str1과 str2는 new String을 사용해서 각각 인스턴스 생성, 동일성에는 성립X
둘은 내부에 같은 hello를 갖고 있기 때문에 논리적으로는 동일 (동등성 성립)


String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열을 풀에 사용

자바가 실행되는 시점에 클래스에 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어 둔다. 같은 문자열이 있다면 생성 X

String str3 = "hello" 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾는다, 그리고 찾은 인스턴스의 참조를 반환

String str4 = "hello" str3에서 사용한 참조를 사용

문자열 리터럴을 사용하는 경우 == 비교에도 성립

참고)
pool은 자원이 모여있는 곳을 의미, 프로그래밍에서 풀은 공용 자원을 모아둔 곳.
여러 곳에서 함께 사용할 수 있는 객체를 필요할 때마다 생성, 제거하는 것은 비효율적.
대신 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고, 여러곳에서 재사용 할 수 있다면, 메모리를 더 최적화

문자열 리터럴을 사용하면 == 비교를 하고, new String을 직접 사용하는 경우 equals 사용 하면 되지 않을까?

public class StringEqualsMain2 {
    public static void main(String[] args) {
        String str1 = new String("Hello");
        String str2 = new String("Hello");
        System.out.println("메서드 호출1 = " + isSame(str1, str2));

        System.out.println("리터럴 비교");
        String str3 = "Hello";
        String str4 = "Hello";
        System.out.println("메서드 호출2 = " + isSame(str3, str4));

    }

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

다른 개발자가 이코들 봤을때 isSame()의 변수가 넘어오는 String 인스턴스가 new String인지 문자열 리터럴로 만들어 진 것인지 확인 X

String 클래스 - 불변 객체

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

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

String은 불변객체이다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환

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

String이 불변으로 설계된 이유

String이 불변으로 설계된 이유는 문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면, 같은 문자열을 참고하는 다른 변수의 값도 함께 변경

  • String은 자바 내부에서 문자열 풀을 통해 최적화
  • 만약 String 내부에 값을 변경할 수 있다면, 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제 발생 => 사이드 이펙트

하지만 String은 불변으로 설계되어 사이드 이펙트 문제 말생 X

String 주요 메서드 1

문자열 정보 조회

  • length() : 문자열의 길이를 반환한다.
  • isEmpty() : 문자열이 비어 있는지 확인한다. (길이가 0)
  • isBlank() : 문자열이 비어 있는지 확인한다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11
  • charAt(int index) : 지정된 인덱스에 있는 문자를 반환한다.

문자열 비교

  • 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) : 문자열의 끝에 다른 문자열을 붙인다.
  • replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체 한다.
  • replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체한다.
  • replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체한다.
  • toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환한다.
  • trim() : 문자열 양쪽 끝의 공백을 제거한다. 단순 Whitespace 만 제거할 수 있다.
  • strip() : Whitespace 와 유니코드 공백을 포함해서 제거한다. 자바 11

문자열 분할 및 조합

  • split(String regex) : 문자열을 정규 표현식을 기준으로 분할한다.
  • join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합한다.

기타 유틸리티

  • valueOf(Object obj) : 다양한 타입을 문자열로 변환한다.
  • toCharArray(): 문자열을 문자 배열로 변환한다.
  • format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성한다.
  • matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인한다.

문자열 정보 조회

  • length : 문자열 길이를 반환
  • isEmpty : 문자열이 비어 있는지 확인한다.
  • isBlank : 문자열이 비어있는지 확인 (길이가 0이거나 공백만 있는 경우)
  • charAt(int index) : 지정된 인덱스에 있는 문자를 반환
public class StringInfoMain {
    public static void main(String[] args) {
        String str = "hello, Java";
        System.out.println("str.length() = " + str.length());
        System.out.println("str.isEmpty() = " + str.isEmpty());
        System.out.println("str.isBlank() = " + str.isBlank());
        System.out.println("str = " + "      ".isBlank());

        char c = str.charAt(7);
        System.out.println("c = " + c);
    }
}

문자열 비교

  • equals(Object anObject) : 두 문자열이 동일한지 비교한다.
  • equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교한다.
  • compareTo(String anotherString) : 두 문자열을 사전 순으로 비교한다.
  • compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교한다.
  • startsWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인한다.
  • endsWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인한다.
public class StringComparisonMain {
    public static void main(String[] args) {
        String str1 = "Hello Java";
        String str2 = "hello java";
        String str3 = "Hello World";

        System.out.println(str1.equals(str2));
        System.out.println(str1.equalsIgnoreCase(str2));

        System.out.println("b".compareTo("a"));
        System.out.println(str1.compareTo(str3));
        System.out.println(str1.compareToIgnoreCase(str2));

        System.out.println(str1.startsWith(str2));
        System.out.println(str1.endsWith("Java"));
    }
}

문자열 검색

  • contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인한다.
  • indexOf(String ch) / indexOf(String ch, int fromIndex) : 문자열이 처음 등장하는 위치를 반환한다.
  • lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환한다.
public class StringSearchMain {
    public static void main(String[] args) {
        String str = "Hello Java Welcome to JavaWorld";
        System.out.println(str.contains("Java"));
        System.out.println(str.indexOf("Java"));
        System.out.println(str.indexOf("Java", 10));
        System.out.println(str.length());
    }
}

String 클래스 - 주요 메서드 2

문자열 조작 및 변환

  • substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환한다.
  • concat(String str) : 문자열의 끝에 다른 문자열을 붙인다.
  • replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체 한다.
  • replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체한다.
  • replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체한다.
  • toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환한다.
  • trim() : 문자열 양쪽 끝의 공백을 제거한다. 단순 Whitespace 만 제거할 수 있다.
  • strip() : Whitespace 와 유니코드 공백을 포함해서 제거한다. 자바 11
public class StringChangeMain1 {
    public static void main(String[] args) {
        String str = "Hello Java welcome to Java";

        System.out.println(str.substring(7));
        System.out.println(str.substring(7, 10));

        System.out.println(str.concat("!!!"));
        System.out.println(str.replace("Java", "World"));
        System.out.println(str.replaceFirst("Java", "World"));
    }
}
public class StringChangeMain2 {
    public static void main(String[] args) {
        String str = "          Java Programming            ";
        System.out.println(str.toLowerCase());
        System.out.println(str.toUpperCase());

        System.out.println(str.trim() + "'");
        System.out.println(str.strip() + "'");
        System.out.println(str.stripLeading() + "'");
        System.out.println(str.stripTrailing() + "'");

    }
}

문자열 분할 및 조합

  • split(String regex) : 문자열을 정규 표현식을 기준으로 분할한다.
  • join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합한다.
public class StringSpiltJoinMain {
    public static void main(String[] args) {
        String str = "Apple,Banana,Orange";

        //split
        String[] strSplit = str.split(",");
        for (String s : strSplit) {
            System.out.println(s);
        }

        //join
        String joinedStr = String.join("-", "A", "B", "C");
        System.out.println(joinedStr);

        //문자열 배열 연결
        String rst = String.join("-" , strSplit);
        System.out.println("rst = " + rst);
    }
}

기타 유틸리티

  • valueOf(Object obj) : 다양한 타입을 문자열로 변환한다.
  • toCharArray(): 문자열을 문자 배열로 변환한다.
  • format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성한다.
  • matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인한다.
public class StringUtilsMain1 {
    public static void main(String[] args) {
        int num = 100;
        boolean bool = true;
        Object o = new Object();
        String str = "Hello Java";

        //valueof 메서드
        String numString = String.valueOf(num);
        System.out.println("numString = " + numString);

        String boolString = String.valueOf(bool);
        System.out.println("boolString = " + boolString);

        String objString = String.valueOf(o);
        System.out.println("objString = " + objString);

        String numString2 = "" + num;
        System.out.println("numString2 = " + numString2);


        //toCharArray 메서드
        char[] strCharArray = str.toCharArray();
        System.out.println("strCharArray = " + strCharArray);
        for (char c : strCharArray) {
            System.out.println("c = " + c);
        }
        System.out.println();
     }
}
public class StringUtilsMain2 {
    public static void main(String[] args) {
        int num = 100;
        boolean bool = true;
        String str = "Hello Java";

        //format 메서드
        String format = String.format("num : %d, bool = %b, str = %s", num, bool, str);
        System.out.println("format = " + format);

        String format1 = String.format("숫자: %.2f", 10.1234);
        System.out.println("format1 = " + format1);

        //printf
        System.out.printf("숫자 : %.2f\n", 10.1234);


        //matches 메서드
        //Java or World가 들어오면 match가 되는가
        String regex = "Hello, (Java!|World!)";
        System.out.println(str.matches(regex));
    }
}

StringBuilder - 가변 String

불변인 String의 단점

"A" + "B"
String("A") + String("B") //문자는 String 타입이다.
String("A").concat(String("B"))//문자의 더하기는 concat을 사용한다.
new String("AB") //String은 불변이다. 따라서 새로운 객체가 생성된다.

불변인 String의 내부 값은 변경 X, 변경된 값을 기반으로 새로운 String 객체를 생성

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

불변인 String 클래스의 단점은 문자를 더하거나 변경시 계속 새로운 객체를 생성해야 하는 단점

문자를 자주 더하거나 변경해야 하는 상황이라면, 더 많은 String 객체를 만들고 GC를 해야 한다.
그러면 CPU 메모리를 더 많이 사용.

StringBuilder

가변의 경우 사이드 이펙트에 주의

public final class StringBuilder {
 char[] value;// 자바 9 이전
 byte[] value;// 자바 9 이후
 //여러 메서드
 public StringBuilder append(String str) {...}
 public int length() {...}
 ...
}

StringBuilder 내부에는 final이 아닌 변경할 수 있는 byte[]

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

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

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

        //StringBuilder -> String
        String string = sb.toString();
        System.out.println("string = " + string);
    }
}
  • append : 여러 문자열 추가
  • insert : 특정 위치에 문자열을 삽입
  • delete : 특정 문자열 삭제
  • reverse : 문자열 뒤집음
  • toString 메소드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환

가변 VS 불변
String은 불변하다. 즉 한번 생성되면, 내용을 변경 X -> 문자열에 변화를 주려고 할때마다 새로운 인스턴스 생성, 메모리 소모

StringBuilder는 가변적. 객체 안에서 문자열을 추가, 삭제, 수정. 메모리 사용을 줄이고, 성능을 향상

마지막에 안전한 불변으로 (String)으로 변경하는 것이 좋다

String 최적화

  1. 문자열 리터럴 최적화

컴파일 전

String helloWorld = "Hello, " + "World!";

컴파일 후

String helloWorld = "Hello, World!";
  1. String 변수 최적화

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

String result = str1 + str2;

이런경우 최적화를 수행

String result = new StringBuilder().append(str1).append(str2).toString();

String 최적화 어려운 경우

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

        String rst = "";
        for (int i = 0; i < 100000; i++) {
            rst += "Hello Java";
        }

        long endTime = System.currentTimeMillis();

        System.out.println("rst = " + rst);
        System.out.println("Time = " + (endTime - startTime));
    }
}

반복문의 루프 내부에서는 최적화가 일어나는 것 처럼 보이지만, 반복 회수 만큼 객체를 생성.
반복문 내에서의 문자열 연결은 런타임에 연결한 문자열의 개수와 내용이 결정. 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복문에서 문자열이 어찌 변할지 알수 X

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 rst = sb.toString();
        long endTime = System.currentTimeMillis();

        System.out.println("rst = " + rst);
        System.out.println("Time = " + (endTime - startTime));
    }
}

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

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

참고: StringBuilder vs StringBuffer

StringBuilder 와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다.

StringBuffer 는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느리다.

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

메서드 체이닝

public class ValueAdder {
    private int value;

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

    public int getValue(){
        return value;
    }
}
public class MethodChangingMain1 {
    public static void main(String[] args) {
        ValueAdder adder = new ValueAdder();
        adder.add(1);
        adder.add(2);
        adder.add(3);

        int rst = adder.getValue();
        System.out.println("rst = " + rst);
    }
}
public class MethodChangingMain2 {
    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 rst = adder3.getValue();
        System.out.println("rst = " + rst);

        System.out.println("adder1 = " + adder1);
        System.out.println("adder2 = " + adder2);
        System.out.println("adder3 = " + adder3);
    }
}

  1. adder.add(1) 을 호출한다.
  2. add() 메서드는 결과를 누적하고 자기 자신의 참조값인 this ( x001 )를 반환한다.
  3. adder1 변수는 adder 와 같은 x001 인스턴스를 참조한다

  • add 메서드는 자기 자신 this의 참조값을 반환
    반환 값을 adder1, adder2, adder3에 보관

  • adder, adder1, adder2, adder3은 모두 같은 참조값을 사용.

새로운 변수에 담아서 보관하지 않고 메서드 호출에 사용

public class MethodChangingMain3 {
    public static void main(String[] args) {
        ValueAdder adder = new ValueAdder();
        int rst = adder.add(1).add(2).add(3).getValue();
        //참조값에 .찍어서 호출 가능 바로 호출 가능
        //메서드가 chain 처럼 연결
        System.out.println("rst = " + rst);
    }
}

메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있다.

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

메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문이다. 이 참조값에 . 을 찍어서 바로 자신의 메서드를 호출할 수 있다.

StringBuilder 메서드 체인

public StringBuilder append(String str) {
 super.append(str);
 return this;
}
profile
개발자를 향해 가는 중입니다~! 항상 겸손

0개의 댓글