[Java] String 클래스

szlee·2024년 8월 12일
0

Java

목록 보기
26/34

< 김영한의 실전 자바 - 중급편 > 강의를 보고 이해한 내용을 바탕으로 합니다.





자바에서 문자를 다루는 타입 대표 두가지 : char, String
char는 문자 하나를 다룰 때 사용하고, String은 문자열을 다룰 때 사용한다.
(char를 사용해서 여러 문자를 나열하려면 char[]를 사용해야한다.)

//char
char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'};

//String
String str = "hello";

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

  • 쌍따옴표 "hello"
  • 객체 생성 new String("hello");
String str1 = "hello";
String str2 = new String("hello");

String은 클래스이므로 참조형이다. (int, boolean 등은 기본형.)
따라서 str1변수에는 String인스턴스의 참조값만 들어갈 수 있다.
그래서 String str1 = "hello"; 이 코드는 이상하게 보일 수 있는데,
문자열은 매우 자주 사용되기 때문에 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello");와 같이 변경해준다.



String 클래스의 구조

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[]에 보관된다.
(직접 다루기 불편한 char[]을 내부에 감추고 String클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있게 한다.)

자바 9 이후 String클래스 변경 사항

자바 9 부터 String 클래스에서 char[] 대신 byte[]를 사용한다.

private final byte[] value;

char는 2byte를 차지한다. 그런데 영어, 숫자는 보통 1byte로 표현이 가능하기 때문에 단순 영어, 숫자로만 표현된 경우 1byte를 사용하고 나머지의 경우 2byte인 UTF-16 인코딩을 사용하여 메모리를 더 효율적으로 사용할 수 있게되었다.

기능(메서드)

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

(그 외에도 많으니 필요한 것들 직접 검색해볼것..)



String 클래스와 참조형

String은 클래스이므로 참조형이다.
참조형은 변수에 계산할 수 있는 값이 들어있는 것이 아니라, 계산할 수 없는 참조값이 들어있다. 따라서 원칙적으로는 +같은 연산을 사용할 수 없다.
자바에서 문자열을 더할 때 String이 제공하는 concat()과 같은 메서드를 사용해야 하나, 문자열은 너무 자주 다루어지기 때문에 특별히 +연산을 제공한다.

⭐️ 비교

String 클래스를 비교할 때는 ==가 아니라, equals()를 사용해야한다.

  • 동일성 : == 사용. 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
  • 동등성 : equals() 사용. 두 객체가 논리적으로 같은지 확인
String str1 = new String("hello");
String str2 = new String("hello");

System.out.println("new String() == 비교: " + (str1 == str2)); //false
System.out.println("new String() equals 비교: " + (str1.equals(str2))); //true


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

System.out.println("리터럴 == 비교: " + (str3 == str4)); //true
System.out.println("리터럴 equals 비교: " + (str3.equals(str4))); //true

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

문자열 풀

String str3 = "hello"; 와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.
자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둔다. 이 때, 같은 문자열이 있으면 만들지 않는다.
찾은 인스턴스의 참조값을 반환한다.
따라서 str3과 str4는 같은 참조를 사용한다.=> 동일성(==)비교에도 성공한다.

문자열 리터럴을 사용하면 == 비교를 하고, new String() 을 직접 사용하는 경우에만 equals() 비교를 사용해도 될까?

public static void main(String[] args) {
    String str1 = new String("hello");
    String str2 = new String("hello"); 
    System.out.println("메서드 호출 비교1: " + isSame(str1, str2)); //false
    
    String str3 = "hello";
    String str4 = "hello";
    System.out.println("메서드 호출 비교2: " + isSame(str3, str4)); //true
}

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

main() 메서드와 isSame()메서드를 만드는 개발자가 서로 다르다고 가정.
isSame()의 경우 매개변수로 넘어오는 인스턴스가 new String인지, 문자열 리터럴로 만들어진 것인지 확인할 수 없다.
따라서 문자열 비교는 항상 equals()를 사용해서 동동성 비교를 해야한다.



불변 객체

String은 불변 객체이기 때문에 생성 이후에 절대 내부의 문자열 값을 변경할 수 없다.
따라서 변경이 필요한 경우 기존 값을 변경하지 않고 새로운 결과를 만들어서 반환한다.

String str1 = "hello";
// str1.concat(" java") 만 한 뒤에 str1을 출력해보면 여전히 hello다.
String str2 = str1.concat(" java");

왜 String이 불변으로 설계되었을까?
문자열 풀에 있는 String의 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경되기 때문이다.



주요 메서드

문자열 정보 조회

  • 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) : 문자열이 주어진 정규 표현식과 일치하는지 확인한다.



StringBuilder - 가변 String

불변인 String 클래스의 단점

String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D"); //문자는 String 타입이다.
String str = new String("AB") + String("C") + String("D"); //String은 불변이므로 새로운 객체가 생성된다.
String str = new String("ABC") + String("D");
String str = new String("ABCD");

불변인 String의 내부 값은 변경할 수 없으므로 새로운 String 객체가 생성된다.
위와 같은 경우 총 세개의 String 클래스가 추가로 생성된다.
그런데 중간에 만들어진 애들은 사용되지 않고 이후 GC의 대상이 된다.
즉, 불변인 String 클래스의 단점은 문자를 더할 때마다 계속해서 객체를 생성해야 한다는 것이다. 문자를 자주 더하거나 변경하는 경우 결과적으로 컴퓨터의 CPU와 메모리 자원을 더 많이 사용하게되고, 문자열의 크기가 클수록 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모한다.

StringBuilder

가변 String을 쓰면 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없다. 따라서 성능과 메모리에도 더 효율적이다.
StringBuilder는 가변 String이다. 내부에 final이 아닌 변경할 수 있는 byte[]를 가지고 있다.

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

StringBuilder 사용하기

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 = ABCD
         
        sb.insert(4, "Java");
        System.out.println("insert = " + sb); //insert = ABCDJava
        
        sb.delete(4, 8);
        System.out.println("delete = " + sb); //delete = ABCD
        
        sb.reverse();
        System.out.println("reverse = " + sb);
        
        //StringBuilder -> String
        String string = sb.toString(); //reverse = DCBA
        System.out.println("string = " + string); //string = DCBA
    }
  • append() : 여러 문자열 추가
  • insert() : 특정위치에 문자열 삽입
  • delete() : 특정위치의 문자열 삭제
  • reverse() : 문자열 뒤집기
  • toString() : StringBuilder의 결과를 기반으로 String을 생성해서 반환

StringBuilder는 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋다.



String 최적화

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

문자열 리터럴 최적화

런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상된다.

컴파일 전

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

컴파일 후

String helloWorld = "Hello, World!";

String 변수 최적화

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

String result = str1 + str2;

이런 경우에 예를 들어 다음과 같이 최적화를 수행한다.

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

(자바 9 부터는 StringConcatFactory를 사용해서 최적화 수행)

=> 자바가 최적화를 처리해주기 때문에 간단한 경우에는 StringBuilder를 사용하지 않아도 된다.

String 최적화가 어려운 경우

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

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

최적화는 다음과 같이 이루어진다.

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

반복 횟수만큼 객체를 생성해야 한다.
반복문 내에서의 문자열 연결은 런타임에 연결할 문자열의 개수와 내용이 결정된다.
이런 경우에 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다. --> 최적화가 어렵다.

문자열을 합칠 때 대부분의 경우 최적화가 되므로 +연산을 사용하면 된다.

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

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

StringBuilder vs StringBuffer

StringBuilder 와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다.
StringBuffer 는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느리다.
StringBuilder 는 멀티 쓰레드에 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠르다.



메서드 체이닝

public class ValueAdder {
	private int value;
    
 	public ValueAdder add(int addValue) {
 		value += addValue;
 		return this; //자신(this)의 참조값을 반환
 	}
    
 	public int getValue() {
 		return value;
 	}
}

add() 메서드를 호출하면 자신의 참조값이 반화된다.
이 반환된 참조값을 변수에 담아두지 않아도 된다.
대신 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있다.

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
}

메서드 호출의 결과로 자기 자신의 참조값을 반환하면 반환된 참조값을 사용해서 메서드 호출을 이어갈 수 있다. .을 찍고 메서드를 계속 연결해서 사용하는데 이 모습이 마치 메서드가 체인으로 연결된 것처럼 보인다. => 메서드 체이닝.
(메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문이다.)
메서드 체이닝은 코드를 간결하고 읽기 쉽게 만들어준다.

StringBuilder와 메서드 체인

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

public StringBuilder append(String str) {
 	super.append(str);
 	return this;
}

위 코드를 보면, StringBuilder의 append() 메서드에서 자기 자신의 참조값을 반환한다.
StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝을 제공하기 위해 자기 자신을 반환한다. --insert(), delete(), reverse()

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
 }
profile
🌱

0개의 댓글