일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링 mvc1 - 스프링 mvc
- 2024 정보처리기사 수제비 실기
- 스프링 고급 - 스프링 aop
- 2024 정보처리기사 시나공 필기
- 스프링 mvc2 - 로그인 처리
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch8
- 자바 기본편 - 다형성
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch13
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 타임리프
- 게시글 목록 api
- 자바의 정석 기초편 ch3
- 스프링 db1 - 스프링과 문제 해결
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch7
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch4
- 스프링 mvc2 - 검증
- 스프링 입문(무료)
- 자바의 정석 기초편 ch11
- @Aspect
- Today
- Total
나구리의 개발공부기록
String 클래스(기본, 비교, 불변 객체, 주요 메서드), StringBuilder - 가변 String, String 최적화, 메서드 체이닝(Method Chaining) 본문
String 클래스(기본, 비교, 불변 객체, 주요 메서드), StringBuilder - 가변 String, String 최적화, 메서드 체이닝(Method Chaining)
소소한나구리 2025. 1. 18. 17:01출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. String 클래스
1) 기본
(1) CharArrayMain
- 자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있음
- 기본형인 char는 문자를 하나 다룰 때 사용하며 여러 문자를 나열하려면 char[]을 사용해야 함
- 하지만 char[]을 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공함
package lang.String;
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
*/
(2) StringBasicMain
- String클래스를 통해 문자열을 생성하는 방법은 2가지가 있음
- 쌍따옴표 사용: "hello"
- 객체 생성: new String("hello")
- String은 클래스이기 때문에 참조형이므로 str1, str2 변수에는 String 인스턴스의 참조값만 들어갈 수 있지만 기본형처럼도 입력할 수 있음
- 문자열은 매우 자주 사용되기 때문에 편의상 기본형처럼 입력할 수 있도록 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")와 같이 변경해줌
- 그리고 성능 최적화를 위해 문자열 풀을 사용하는데 이부분은 뒤에서 설명함
package lang.String;
public class StringBasicMain {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = new String("Hello");
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
}
}
(3-1) String 클래스 구조
- String 클래스는 간략하게만 보면 아래처럼 생겼으며 클래스이므로 속성(필드)와 기능(메서드)를 가짐
- 자바9 이전에는 char[]을 가지고 있었으나 자바9 이후에는 byte[]을 가지고 있음
public final class String {
//문자열 보관
private final char[] value;// 자바 9 이전
private final byte[] value;// 자바 9 이후
//여러 메서드
public String concat(String str) {...}
public int length() {...}
...
}
(3-2) String 클래스 속성(필드)
- private final char[] value
- String의 실제 문자열값이 보관되며 문자 데이터 자체는 char[]에 보관됨
- String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 편리하게 사용할 수 있도록 다양한 기능을 제공하며 자바 언어 차원에서도 여러 편의 문법을 제공함
** 참고
- 자바 9 이후부터 String 클래스에서 char[] 대신에 byte[]를 사용함
- 자바에서 문자 하나를 표현하는 char는 2byte를 차지하는데 영어, 숫자는 1byte로 표현이 가능하기 때문에 1byte를 사용하고(Latin-1 인코딩의 경우에만), 나머지의 경우는 2byte인 UTF-16 인코딩을 사용하여 메모리를 더 효율적으로 사용할 수 있도록 변경되었음
(3-3) String 클래스 기능(메서드)
- String 클래스는 문자열로 처리할 수 있는 매우 다양한 기능을 제공하므로 필요한 기능이 있으면 검색하거나 API 문서를 찾아서 사용하면 됨
- 메서드의 자세한 내용은 뒤에서 다시 배우므로 주요 메서드 몇개만 작성
- length(): 문자열의 길이 반환
- charAt(int index): 특정 인덱스의 문자를 반환
- substring(int beginIndex, int endIndex): 문자열의 부분 문자열을 반환
- indexOf(String str): 특정 문자열이 시작되는 인덱스를 반환
- toLowerCase(), toUpperCase(): 문자열을 소문자 혹은 대문자로 변환
- trim(): 문자열 양 끝의 공백을 제거
- concat(String str): 문자열을 더함
(4) StringConcatMain - String 클래스와 참조형
- String은 클래스이므로 기본형이 아니라 참조형이므로 원칙적으로 + 같은 연산을 사용할 수 없으므로 String 클래스에서 제공하는 concat()과 같은 메서드를 사용해야함
- 그러나 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공함
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
*/
2) 비교
(1) StringEqualsMain1
- String 클래스를 비교할 때는 동일성 비교인 == 비교가 아니라 항상 equals() 메서드로 동등성 비교를 해야함
- new String()으로 동일한 문자열을 값으로 생성한 객체들을 == 비교를 하면 당연히 false가 나오지만 String 클래스의 equals()는 오버라이딩이 되어 있기 때문에 equals() 비교를 하면 true가 나옴
- 그러나 더 재밌는것은 String은 클래스이므로 참조형이지만 == 비교를 해도 true가 됨
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("new String() == 비교: " + (str1 == str2));
System.out.println("new String() equals 비교: " + (str1.equals(str2)));
String str3 = "hello";
String str4 = "hello";
System.out.println("리터럴 == 비교: " + (str3 == str4));
System.out.println("리터럴 equals 비교: " + (str3.equals(str4)));
}
}
/* 실행 결과
new String() == 비교: false
new String() equals 비교: true
리터럴 == 비교: true
리터럴 equals 비교: true
*/
(1-2) new String() 비교
- str1과 str2는 new String()을 사용하여 각각 인스턴스를 생성했기 때문에 서로 다른 인스턴스이므로 동일성 실패에 비교함
- 둘은 내부에 같은 "hello" 값을 가지고 있기 때문에 논리적으로 같고 String 클래스가 내부 문자열 값을 비교하도록 equals()메서드를 재정의 해두었기 때문에 동등성(equals()) 비교에 성공함
(1-3) 문자열 리터럴, 문자열 풀
- String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용함
- 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어두는데 이때 같은 문자열이 있으면 만들지 않음
- String str3 = "hello" 처럼 문자열을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾은 뒤 인스턴스의 참조값을 반환함
- String str4 = "hello" 로 한번더 입력해도 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 찾은 인스턴스의 참조값을 반환하게 되어 str3와 str4는 같은 참조값을 사용함
- 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있음
- 문자열 리터럴을 사용하면 같은 참조값을 가지기 때문에 == 비교에 성공함
** 참고
- 풀(Pool)은 자원이 모여있는 곳을 의미하며 프로그래밍에서 풀은 공용 자원을 모아둔 곳을 뜻함
- 여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고 제거하는 것은 비효율적이므로 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 최적화 할 수 있음
- 문자열 풀은 힙 영역을 사용하며 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있음
- 해시 알고리즘은 컬렉션에서 설명함
(2) StringEqualsMain2
- 위의 내용들을 근거로 문자열 리터럴을 사용하면 == 비교를 하고 new String()을 직접 사용하는 경우에는 equals() 비교를 사용하면 될거라고 생각할 수 있는데 그러면 안됨
- main() 메서드를 만드는 개발자와 isSame() 메서드를 만드는 개발자가 서로 다르다고 가정해보면 isSame()의 매개변수로 넘어오는 String 인스턴스가 new String()으로 만들어졌는지, 문자열 리터럴로 만들어진 것인지 확인할 수 있는 방법이 없음
- 즉, 문자열 비교는 항상 equals()를 사용하여 동등성 비교를 해야함
package lang.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));
String str3 = "hello";
String str4 = "hello";
System.out.println("메서드 호출 비교2: " + isSame(str3, str4));
}
private static boolean isSame(String str1, String str2) {
return str1 == str2;
// return str1.equals(str2);
}
}
/* 실행 결과
메서드 호출 비교1: false
메서드 호출 비교2: true
*/
3) 불변 객체
(1) StringImmutable1
- String 클래스 내부에 들어가보면 필드가 final로 되어있는 불변 객체이므로 절대로 내부의 문자열 값을 변경할 수 없음
- String이 제공하는 concat() 메서드를 사용하면 기존 문자열에 새로운 문자열을 연결해서 합칠 수 있는데, 실행 결과를 보면 문자가 전혀 합쳐지지 않았음
package lang.String.immutable;
public class StringImmutable1 {
public static void main(String[] args) {
String str = "hello";
str.concat(" java");
System.out.println("str = " + str);
}
}
/* 실행 결과
str = hello
*/
(2) StringImmutable2
- String은 불변 객체이기 때문에 변경이 필요한 경우 기존값을 변경하지 않고 새로운 결과를 만들어서 반환함(이 내용은 바로 이전의 강의의 불변객체에서 배움)
- String이 제공하는 concat()메서드의 내부를 보면 새로운 String 객체를 만들어서 반환하는 것을 확인할 수 있음
package lang.String.immutable;
public class StringImmutable2 {
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
*/
(3) String이 불변으로 설계된 이유
- String 클래스가 만약 가변이라면 문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경되게 됨
- String은 자바 내부에서 문자열 풀을 통해 최적화를 하는데 만약 String 내부의 값을 변경할 수 있다면 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 사이드 이펙트 문제가 발생하게 됨
- String 클래스가 불변으로 설계되었기 때문에 이런 사이드 이펙트 문제가 발생하지 않음
4) 주요 메서드
** 참고
- String 클래스는 문자열을 편리하게 다루기 위한 다양한 메서드를 제공하는데 여기서는 자주 사용하는 기능 위주로 나열
- 기능이 너무 많기 때문에 외우기보다는 주로 사용하는 메서드가 어떤것이 있는지 대략 알아두고 필요할 때 검색을 통해 원하는 기능을 찾는것이 좋음
(1) 문자열 정보 조회 - StringInfoMain
- length(): 문자열의 길이를 반환
- isEmpty(): 문자열이 비어있는지 확인, 길이가 0
- isBlank(): 문자열이 비어있는지 확인, 길이가 0이거나 공백만 있는경우, 자바 11부터 지원
- charAt(int index): 지정된 인덱스에 있는 문자를 반환
package lang.String.method;
public class StringInfoMain {
public static void main(String[] args) {
String str = "Hello, Java!";
System.out.println("문자열의 길이: " + str.length());
System.out.println("문자열이 비어 있는지: " + str.isEmpty());
System.out.println("문자열이 비어 있거나 공백인지 : " + str.isBlank());
System.out.println("문자열이 비어 있거나 공백인지 : " + " ".isBlank());
char c = str.charAt(7);
System.out.println("7번 인덱스의 문자 = " + c);
}
}
/* 실행 결과
문자열의 길이: 12
문자열이 비어 있는지: false
문자열이 비어 있거나 공백인지 : false
문자열이 비어 있거나 공백인지 : true
7번 인덱스의 문자 = J
*/
(2) 문자열 비교 - StringComparisonMain
- equals(Object anObject): 두 문자열이 동일한지 비교
- equalsIgnoreCase(String anotherString): 두 문자열을 대소문자 구분없이 비교
- compareTo(String anotherString): 두 문자열을 사전 순으로 비교한 후 다른 단어를 만나면 그 차이를 int로 반환,
- compareToIgnoreCase(String str): 위와 동일한 기능을 대소문자 구분없이 비교
- startsWith(String prefix): 문자열이 특정 접두사로 시작하는지 확인
- endsWith(String suffix): 문자열이 특정 접미사로 끝나는지 확인
package lang.String.method;
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: " + str1.equals(str2));
System.out.println("str1 equals str2: " + str1.equalsIgnoreCase(str2));
System.out.println("'b' compareTo 'a': " + "b".compareTo("a"));
System.out.println("'c' compareTo 'a': " + "c".compareTo("a"));
System.out.println("str1 compareTo str3: " + str1.compareTo(str3));
System.out.println("str1 compareToIgnoreCase str2: " + str1.compareToIgnoreCase(str2));
System.out.println("str1 starts with 'Hello': " + str1.startsWith("Hello"));
System.out.println("str1 ends with 'Java!': " + str1.endsWith("Java!"));
}
}
/* 실행 결과
str1 equals str2: false
str1 equals str2: true
'b' compareTo 'a': 1
'c' compareTo 'a': 2
str1 compareTo str3: -13
str1 compareToIgnoreCase str2: 0
str1 starts with 'Hello': true
str1 ends with 'Java!': true
*/
(3) 문자열 검색 - StringSearchMain
- contains(CharSequence s): 문자열이 특정 문자열을 포함하고 있는지 확인
- indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환
- lastIndexOf(String ch): 문자열이 마지막으로 등장하는 위치를 반환
package lang.String.method;
public class StringSearchMain {
public static void main(String[] args) {
String str = "Hello, Java! Welcome to Java world.";
System.out.println("문자열에 'Java'가 포함되어 있는지: " + str.contains("Java"));
System.out.println("'Java'의 첫 번째 인덱스: " + str.indexOf("Java"));
System.out.println("인덱스 10부터 'Java'의 인덱스: " + str.indexOf("Java", 10));
System.out.println("'Java'의 마지막 인덱스: " + str.lastIndexOf("Java"));
}
}
/* 실행 결과
문자열에 'Java'가 포함되어 있는지: true
'Java'의 첫 번째 인덱스: 7
인덱스 10부터 'Java'의 인덱스: 24
'Java'의 마지막 인덱스: 24
*/
(4) 문자열 조작 및 변환 - StringChangeMain
- 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부터 지원
package lang.string.method;
public class StringChangeMain {
public static void main(String[] args) {
String str = "Hello, Java! Welcome to Java";
System.out.println("인덱스 7부터 부분 문자열: " + str.substring(7));
System.out.println("인덱스 7부터 12까지의 부분 문자열: " + str.substring(7, 12));
System.out.println("문자열 결합: " + str.concat("!!!"));
System.out.println("'Java'를 'World'로 대체: " + str.replace("Java", "World"));
System.out.println("첫 번째 'Java'를 'World'로 대체: " + str.replaceFirst("Java", "World"));
String strWithSpace = " Java Programming ";
System.out.println("소문자로 변환: " + strWithSpace.toLowerCase());
System.out.println("대문자로 변환: " + strWithSpace.toUpperCase());
System.out.println("공백 제거(trim): '" + strWithSpace.trim() + "'");
System.out.println("공백 제거(strip): '" + strWithSpace.strip() + "'");
System.out.println("앞 공백 제거(strip): '" + strWithSpace.stripLeading() + "'");
System.out.println("뒤 공백 제거(strip): '" + strWithSpace.stripTrailing() + "'");
}
}
/* 실행 결과
인덱스 7부터 부분 문자열: Java! Welcome to Java
인덱스 7부터 12까지의 부분 문자열: Java!
문자열 결합: Hello, Java! Welcome to Java!!!
'Java'를 'World'로 대체: Hello, World! Welcome to World
첫 번째 'Java'를 'World'로 대체: Hello, World! Welcome to Java
소문자로 변환: java programming
대문자로 변환: JAVA PROGRAMMING
공백 제거(trim): 'Java Programming'
공백 제거(strip): 'Java Programming'
앞 공백 제거(strip): 'Java Programming '
뒤 공백 제거(strip): ' Java Programming'
*/
(5) 문자열 분할 및 조합
- split(String regex): 문자열을 정규 표현식을 기준으로 분할
- join(CharSequence delimiter, CharSequence... elements): 주어진 구분자로 여러 문자열을 결합
package lang.String.method;
public class StringSplitJoinMain {
public static void main(String[] args) {
String str = "Apple,Banana,Orange";
// split()
String[] splitStr = str.split(",");
for (String s : splitStr) {
System.out.println(s);
}
// join()
String joinedStr = String.join("-", "Apple", "Banana", "Orange");
System.out.println("연결된 문자열 = " + joinedStr);
// 문자열 배열 연결
String result = String.join("-", splitStr);
System.out.println("result = " + result);
}
}
/* 실행 결과
Apple
Banana
Orange
연결된 문자열 = Apple-Banana-Orange
result = Apple-Banana-Orange
*/
(6) 기타
- valueOf(Object obj): 다양한 타입을 문자열로 변환, 빈 문자열과 + 연산을 하여 간단히 문자열로 변환할 수 있음
- toCharArray(): 문자열을 문자 배열로 변환
- format(String format, Object... args): 형식 문자열과 인자를 사용하여 새로운 문자열을 생성
- matches(String regex): 문자열이 주어진 정규 표현식과 일치하는지 확인
package lang.String.method;
public class StringUtilsMain {
public static void main(String[] args) {
int num = 100;
boolean bool = true;
Object obj = new Object();
String str = "Hello, Java!";
// valueOf 메서드
String numString = String.valueOf(num);
System.out.println("숫자의 문자열 값" + numString);
String boolString = String.valueOf(bool);
System.out.println("불리언의 문자열 값 " + boolString);
String objString = String.valueOf(obj);
System.out.println("객체의 문자열 값 " + objString);
// 문자 + x -> 문자
String numString2 = "" + num;
System.out.println("빈 문자열 + num: " + numString2);
char[] strCharArray = str.toCharArray();
System.out.println("문자열을 문자 배열로 전환: " + strCharArray);
for (char c : strCharArray) {
System.out.print(c);
}
System.out.println();
// format 메서드
String format1 = String.format("num: %d, bool: %b, str: %s", num, bool, str);
System.out.println(format1);
String format2 = String.format("숫자: %.2f", 10.1234);
System.out.println(format2);
// printf
System.out.printf("숫자: %.2f\n", 10.1234);
// matches 메서드
String regex = "Hello, (Java!|World!)";
System.out.println("'str'이 패턴과 일치하는가? " + str.matches(regex));
}
}
/* 실행 결과
숫자의 문자열 값100
불리언의 문자열 값 true
객체의 문자열 값 java.lang.Object@23fc625e
빈 문자열 + num: 100
문자열을 문자 배열로 전환: [C@3f99bd52
Hello, Java!
num: 100, bool: true, str: Hello, Java!
숫자: 10.12
숫자: 10.12
'str'이 패턴과 일치하는가? true
*/
** 참고
- 정규 표현식 위키백과
- 정규 표현식은 별도로 공부가 필요함
- CharSequence는 String, StringBuilder의 상위 타입이며 문자열을 처리하는 다양한 객체를 받을 수 있음
2. StringBuilder - 가변 String
1) StringBuilder
(1) 불변인 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");
- String클래스는 불변이기 때문에 문자를 더하거나 변경할 때마다 계속해서 새로운 객체를 생성해야함
- 문자열 A, B, C, D를 더하게 되면 문자열 ABCD가 생성되는데, 그 과정속에서 만들어지는 AB, ABC는 사용되지 않으므로 가비지컬렉터의 대상이 됨
- 즉, 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String객체를 만들고 GC해야하기 때문에 결과적으로 컴퓨터의 CPU나 메모리 자원을 더 많이 사용하게 되며 문자열의 크기가 크거나 더 자주 변경할 수록 시스템의 자원은 더 많이 소모하게 됨
** 참고
- 실제로는 문자열을 다룰 때 자바가 내부에서 최적화를 적용하는데, 이 부분은 뒤에서 다룸
(2) StringBuilder 구조 일주
- 이 문제를 해결하는 방법은 단순히 불변이 아닌 가변 String이 존재하면 됨
- 가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없으므로 성능과 메모리 사용면에서 불변보다 더 효율적이지만 가변이기 때문에 사이드 이펙트에 주의해서 사용해야 함
public final class StringBuilder extends AbstractStringBuilder implements ... {
char[] value;// 자바 9 이전
byte[] value;// 자바 9 이후
//여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
** 참고
- 실제로는 상속받고 있는 AbstractStringBuilder에 value속성과 length()메서드가 있음
(3) StringBuilderMain1_1
- StringBuilder 객체를 생성하면 참조변수를 통해 문자열을 조작할 수 있는 다양한 메서드를 사용할 수 있는데, String과 차이점은 가변이기 때문에 반환값 없이 메서드만 호출해도 인스턴스의 값이 변경이 됨
- append(): 여러 문자열을 추가
- insert(): 특정 위치에 문자열을 삽입
- delete(): 특정 범위의 문자열을 삭제
- reverse(): 메서드로 문자열을 뒤집음
- toString(): stringBuilder의 결과를 String을 생성하여 반환
package lang.String.builder;
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);
// 4번째 인덱스에 문자열을 추가
sb.insert(4, "Java!");
System.out.println("insert = " + sb);
// 4번째부터 7번째 인덱스까지 문자열을 삭제
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
*/
(4) 가변 vs 불변
- String은 불변이기 때문에 한 번 생성되면 그 내용을 변경할 수 없으므로 문자열에 변화를 주려고 할 때마다 새로운 String객체가 생성되고 기존 객체는 버려지는데 이 과정에서 메모리와 처리 시간을 더 많이 소모함
- 반면 StringBuilder는 가변이기 때문에 문자열에 변화가 있을 때마다 새로운 객체를 생성하지 않으므로 메모리 사용을 줄이고 성능을 향상시킬 수 있으나 사이드 이펙트를 주의해야함
- StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋음
3. String 최적화
1) 자바의 String 최적화
(1) 문자열 리터럴 최적화
- 자바 컴파일러는 문자열 리터럴을 더하는 부분을 자동으로 합쳐주기 때문에 런타임시 별도의 문자열 결합 연산을 수행하지 않으므로 성능이 향상됨
String helloWorld = "Hello, " + "World!"; // 컴파일 전
String helloWorld = "Hello, World!"; // 컴파일 후
(2) String 변수 최적화
- 문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없는데 아래의 코드처럼 최적화를 수행함
- 최적화 방식은 자바 버전에 따라 달라지며 자바9 부터는 StringConcatFactory를 사용하여 최적화를 수행함
- 즉 자바가 최적화를 처리해주기 때문에 이렇게 간단한 경우에는 StringBuilder를 사용할 필요가 없이 + 연산을 사용하면 충분함
String result = str1 + str2;
String result = new StringBuilder().append(str1).append(str2).toString(); // 자동으로 최적화
2) String 최적화가 어려운 경우
(1) LoopStringMain
- 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않음
- 반복문의 루프 내부에서 컴파일러가 자바의 버전에 따라 최적화를 진행하려고 하겠지만 반복문 내부에서 최적화가 진행될 것이라고 예상할 수 있겠지만 결국에는 반복 횟수만큼 객체를 생성해야함
- 이유는 반복문 내에서의 문자열 연결은 런타임에 연결할 문자열의 개수와 내용이 결정되기 때문에 컴파일러가 얼마나 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없기 때문임
- 즉 최적화를 위한 객체는 물론이고 반복 횟수인 100,000번의 String 객체를 생성했을 것이므로 이런 상황에서는 최적화가 어려움
- 맥북 1M pro 기준 약 2.7초가 걸렸음
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");
}
}
/*
Hello Java가 더해진 결과..
time = 2671ms
*/
(2) LoopStringBuilderMain
- 이럴 때는 직접 StringBuilder를 사용하면 됨
- 반복문 내부에서는 StringBuilder를 사용하여 문자열에 변화를 주고 마지막에 toString()메서드로 String으로 변환하여 출력
- 실행해보면 0.003초로 매우 빠른 속도로 연산이 된 것을 확인할 수 있음
package lang.String.builder;
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 ");
}
long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
/* 실행 결과
... 출력 내용 생략
time = 3ms
*/
(3) 정리
- 문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됨
- 그러나 아래의 경우에는 StringBuilder를 직접 사용하는 것이 더 좋음
- - 반복문에서 반복해서 문자를 연결할 때
- - 조건문을 통해 동적으로 문자열을 조합할 때
- - 복잡한 문자열의 특정 부분을 변경해야 할 때
- - 매우 긴 대용량 문자열을 다룰 때
** 참고 StringBuilder vs StringBuffer
- StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있는데 StringBuffer는 내부에 동기화가 되어있어서 멀티 쓰레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느림
- StringBuilder는 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠름
- 이에 대한 내용은 멀티 쓰레드를 학습하면 이해할 수 있음
4. 메서드 체이닝(Method Chaining)
1) 메서드 체이닝
(1) ValueAdder
- 자신의 값과 add()메서드로 넘어온 값을 누적하는 기능을 제공하는 클래스
- 반환값으로 자기 자신(this)의 참조값을 반환함, 이 부분을 유의해서 봐야함
package lang.String.chaining;
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
(2) MethodChainingMain1
- add() 메서드를 여러번 호출해서 값을 누적하고 더해서 출력
- add() 메서드의 반환값을 사용하지 않았음
package lang.String.chaining;
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 = " + result);
}
}
/*
result = 6
*/
(2-1) MethodChainingMain2
- 반환값을 억지로 사용해보면 이런 코드가 될텐데, 실행결과는 기존과 동일함
package lang.String.chaining;
public class MethodChainingMain2 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder.add(2);
ValueAdder adder3 = adder.add(3);
int result = adder3.getValue();
System.out.println("result = " + result);
}
}
/* 실행 결과 동일 */
(2-2) 그림으로 설명
- adder.add(1)을 호출하면 add()메서드는 결과를 누적 후 자기 자신의 참조값을 반환하고 adder1 변수는 adder와 같은 인스턴스를 참조하게 됨
- 나머지 add()메서드들도 add()메서드가 자기 자신(this)의 참조값을 반환했기 때문에 실행 후 참조값을 저장한 변수에는 모두 같은 참조값을 가지게 됨
(3) MethodChainingMain3
- 2번에서 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않는 대신 바로 메서드 호출에 사용할 수 있음
- 1번의 코드보다 훨씬 간결하고 가독성이 좋아짐
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 = " + result);
}
}
/* 실행 결과 동일 */
(4) 실행 순서 및 정리
- add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값이 반환되는데 이 반환된 참조값을 변수에 담아두지 않는 대신 즉시 사용하여 바로 메서드를 호출할 수 있음
- 메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용하여 메서드 호출을 계속 이어갈 수 있는데 .(dot)을 찍고 메서드를 계속 연결해서 사용함
- 마치 메서드가 체인으로 연결된 처럼 보인다고하여 이러한 기법을 메서드 체이닝이라고 함
- 기존에는 메서드를 호출할 때 마다 계속 변수명에 .(dot)을 찍어야 했지만 메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 .을 찍어서 변수명을 생략할 수 있음
- 메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문이며 이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있음
- 메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줌
(5-1) StringBuilder와 메서드 체인
- StringBuilder는 메서드 체이닝 기법을 제공하며 내부 구조를 들어가서 작성된 메서드를 보면 return this;로 자기 자신의 참조값을 반환하고 있음
- StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신의 참조값을 반환함
(5-2) StringBuilderMain1_2
- 기존의 StringBuilderMain1_1의 코드를 전부 메서드 체이닝을 활용하면 여러가지의 기능을 가독성 좋게 코드를 작성할 수 있음
package lang.String.builder;
public class StringBuilderMain1_2 {
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
*/
(6) 정리
- "만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다"
- 메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리해짐
- 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용함
5. 문제와 풀이
1) startsWith
(1) 문제 설명
- startsWith()를 사용하여 url이 https://로 시작하는지 확인
package lang.string.test;
public class TestString1 {
public static void main(String[] args) {
String url = "https://www.example.com";
// 코드 작성
}
}
실행 결과
true
(2) 정답
package lang.string.test;
public class TestString1 {
public static void main(String[] args) {
String url = "https://www.example.com";
System.out.println(url.startsWith("https://"));
}
}
2) length()
(1) 문제 설명
- length()를 사용하여 arr 배열에 들어있는 모든 문자열의 길이 합을 구하여 출력
package lang.string.test;
public class TestString2 {
public static void main(String[] args) {
String[] arr = {"hello", "java", "jvm", "spring", "jpa"};
// 코드 작성
}
}
실행 결과
hello:5
java:4
jvm:3
spring:6
jpa:3
sum = 21
(2) 정답
package lang.string.test;
public class TestString2 {
public static void main(String[] args) {
String[] arr = {"hello", "java", "jvm", "spring", "jpa"};
int sum = 0;
for (String str : arr) {
int strSize = str.length();
System.out.println(str + ":" + strSize);
sum += strSize;
}
System.out.println("sum = " + sum);
}
}
3) indexOf()
(1) 문제 설명
- str에서 ".txt" 문자열이 언제부터 시작하는지 위치를 찾아서 출력
- indexOf()를 사용해야 함
package lang.string.test;
public class TestString3 {
public static void main(String[] args) {
String str = "hello.txt";
// 코드 작성
}
}
실행 결과
index = 5
(2) 정답
package lang.string.test;
public class TestString3 {
public static void main(String[] args) {
String str = "hello.txt";
System.out.println(str.indexOf(".txt"));
}
}
4) substring()
(1) 문제 설명
- substring()을 사용하여 hello 부분과 .txt 부분을 분리, 단순히 subString()에 숫자를 입력하여 문제를 풀면 됨
package lang.string.test;
public class TestString4 {
public static void main(String[] args) {
String str = "hello.txt";
// 코드 작성
}
}
실행결과
filename = hello
extName = .txt
(2) 정답
package lang.string.test;
public class TestString4 {
public static void main(String[] args) {
String str = "hello.txt";
System.out.println("filename = " + str.substring(0, 5));
System.out.println("extName = " + str.substring(5));
}
}
5) indexOf, substring 조합
(1) 문제 설명
- str에는 파일의 이름과 확장자가 주어지고, ext에는 파일의 확장자가 주어질 때 파일명과 확장자를 분리하여 출력
- indexOf()와, substring()을 사용해야함
package lang.string.test;
public class TestString5 {
public static void main(String[] args) {
String str = "hello.txt";
String ext = ".txt";
// 코드 작성
}
}
실행 결과
filename = hello
extName = .txt
(2) 정답
package lang.string.test;
public class TestString5 {
public static void main(String[] args) {
String str = "hello.txt";
String ext = ".txt";
int extIndex = str.indexOf(ext);
System.out.println("filename = " + str.substring(0, extIndex));
System.out.println("extName = " + str.substring(extIndex));
}
}
6) 검색 Count
(1) 문제 설명
- str에서 key로 주어지는 문자를 찾고, 찾은 문자의 수를 출력
- indexOf()를 반복문과 함께 사용
package lang.string.test;
public class TestString6 {
public static void main(String[] args) {
String str = "start hello java, hello spring, hello jpa";
String key = "hello";
// 코드 작성
}
}
실행 결과
count = 3
(2) 정답
package lang.string.test;
public class TestString6 {
public static void main(String[] args) {
String str = "start hello java, hello spring, hello jpa";
String key = "hello";
int count = 0;
int index = str.indexOf(key);
while (index >= 0) {
index = str.indexOf(key, index + 1);
count++;
}
System.out.println("count = " + count);
}
}
- 만약 str.indexOf(key)를 호출하였는데 str에 key가 포함되어있지 않으면 -1을 반환하므로 while 조건문을 0보다 크거나 같을 때 반복하도록 설정
- str에서 key를 찾은 인덱스의 다음 인덱스부터 key를 찾기 위해 str.indexOf(key, index + 1)로 메서드를 호출
7) 공백 제거
(1) 문제 설명
- 문자의 양쪽 공백을 제거
package lang.string.test;
public class TestString7 {
public static void main(String[] args) {
String original = " Hello Java ";
// 코드 작성
}
}
실행 결과
Hello Java
(2) 정답
package lang.string.test;
public class TestString7 {
public static void main(String[] args) {
String original = " Hello Java ";
System.out.println(original.strip());
}
}
- trim()을 사용해도 됨
8) replace
(1) 문제 설명
- replace()를 사용하여 java라는 단어를 jvm으로 변경
package lang.string.test;
public class TestString8 {
public static void main(String[] args) {
String input = "hello java spring jpa java";
// 코드 작성
}
}
실행 결과
hello jvm spring jpa jvm
(2) 정답
package lang.string.test;
public class TestString8 {
public static void main(String[] args) {
String input = "hello java spring jpa java";
String replace = input.replace("java", "jvm");
System.out.println(replace);
}
}
9) split()
(1) 문제 설명
- split()를 사용하여 이메일의 ID 부분과 도메인 부분을 분리
package lang.string.test;
public class TestString9 {
public static void main(String[] args) {
String email = "hello@example.com";
// 코드 작성
}
}
실행 결과
ID: hello
Domain: example.com
(2) 정답
package lang.string.test;
public class TestString9 {
public static void main(String[] args) {
String email = "hello@example.com";
String[] parts = email.split("@");
String id = parts[0];
String domain = parts[1];
System.out.println("ID = " + id);
System.out.println("Domain = " + domain);
}
}
10) split(), join()
(1) 문제 설명
- split()을 사용하여 fruits를 분리하고 join을 사용하여 분리한 문자를 하나로 연결
package lang.string.test;
public class TestString10 {
public static void main(String[] args) {
String fruits = "apple,banana,mango";
// 코드 작성
}
}
실행 결과
apple
banana
mango
joinedString = apple->banana->mango
(2) 정답
package lang.string.test;
public class TestString10 {
public static void main(String[] args) {
String fruits = "apple,banana,mango";
String[] fruitsList = fruits.split(",");
for (String fruit : fruitsList) {
System.out.println(fruit);
}
String joinString = String.join("->", fruitsList);
System.out.println("joinString = " + joinString);
}
}
11) reverse()
(1) 문제 설명
- str문자열을 반대로 뒤집어서 출력하되 StringBuilder의 reverse()를 사용
package lang.string.test;
public class TestString11 {
public static void main(String[] args) {
String str = "Hello Java";
// 코드 작성
}
}
실행 결과
avaJ olleH
(2) 정답
package lang.string.test;
public class TestString11 {
public static void main(String[] args) {
String str = "Hello Java";
StringBuilder sb = new StringBuilder(str);
System.out.println(sb.reverse().toString());
}
}
- StringBuilder로 문자열을 조작한 후 결과는 스트링으로 반환하는 것이 좋음