자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있다.
public static void main(String[] args) {
char a = 'a';
System.out.println("a = " + a);
char[] charArr = new char[]{'h','e','l','l','o'};
System.out.println(charArr);
String str = "hello";
System.out.println(str);
}
char은 문자 하나만 받을 수 있기에 여러개를 받기 위해선 배열을 사용해야한다.
String은 문자열을 받을 수 있다.
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
}
String은 첫 글자가 대문자로 시작하는 만큼 기본형이 아니라 참조형인 것을 알 수 있는데, 따라서 str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있다.
따라서 다음 코드는 뭔가 어색하다
String str1 = "hello";
그러나 자바에서 문자열은 매우 자주 사용된다. 그래서 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")와 같이 변경해준다.(이 경우 실제로는 성능 최적화를 위해 문자열 풀을 사용한다)
String str1 = "hello"; ------> String str1 = new String("hello")
String 클래스는 대략 다음과 같이 생겼다.
private final char[] value;
여기에는 String의 실제 문자열 값이 보관된다. 문자 데이터 자체는 char[]에 보관된다.
String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공한다. 그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공한다.
String의 기능
1. lengh() : 문자열 길이 반환
2. charAt(int index) : 특정 인덱스의 문자를 반환
3. substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환
4. trim() : 문자열 양 끝의 공백을 제거한다
5. concat() : 문자열 더하기
등등
String은 클래스이기 때문에 참조형이다.
참조형은 변수에 계산할 수 있는 값이 들어 있는것이 아니라 x001과 같이 계산할 수 없는 참조값이 들어있다. 따라서 원칙적으로 + 와 같은 연산을 사용할 수 없다.
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);
}
원칙적으로는 불가능 하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.
String 클래스 또한 비교를 할 수 있는데
== 비교가 아닌 equals()비교를 해야한다.
동일성(Identity):== 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
동등성(Equality): euqals()메서드 사용하여 두 객체가 논리적으로 같은지 확인
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)));
}
== 는 참조값(주소값)을 가지고 동일한지 비교를 하는 것이고, equals 는 동일한 문자열인지 비교를 하는 것이다.
참고로 String 클래스에는 내부 문자열 값을 비교하도록 equals() 메서드를 재정의 하였다.
그럼 이제 아래의 코드를 돌려보자
String str3 = "hello";
String str4 = "hello";
System.out.println("리터럴 == 비교: "+(str3==str4));
System.out.println("리터럴 equals 비교: "+(str3.equals(str4)));
이 코드는 독특하게도 둘 다 true가 나온다.
그 이유는 뭘까?
String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.
자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다.
String str3 = "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조(x003)을 반환한다.
문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있다.
String은 불변객체
String은 불변 객체이다. 따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없다.
@Stable
private final byte[] value;
String은 final로 정의되어있다.
public static void main(String[] args) {
String str = "hello";
str.concat(" java");
System.out.println("str = " + str);
}
String.concat() 메서드를 사용하면 기존 문자열에 새로운 문자열을 연결해서 합칠 수 있다.
그러나 실행 결과값은
str = hello
로 문자가 전혀 합쳐지지 않았다.
String은 불변 객체이기 때문이다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환한다.
public static void main(String[] args) {
String str = "hello";
String str2 = str.concat(" java");
System.out.println("str = " + str);
System.out.println("str2 = " + str2);
}
다음은 String에서 제공하는 주요 메서드들이다.
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
public static void main(String[] args) {
String str1 = "Hello, Java!";
String str2 = "hello, java!";
String str3 = "Hello, World!";
System.out.println("str equals str2: " + str1.equals(str2));
System.out.println("str equals str2: " + str1.equalsIgnoreCase(str2));
System.out.println("'b' compareTo 'a': " + "c".compareTo("a") );
//c와 a의 칸 수 차이.
System.out.println("str1 compareTo str3: " + str1.compareTo(str3));
System.out.println("str1 compareTo str2: " + str1.compareToIgnoreCase(str2));
System.out.println("str1 start with 'Hello': " + str1.startsWith("Hello"));
System.out.println("str1 ends with 'Java!': " + str1.endsWith("Java!"));
}
str equals str2: false
str equals str2: true
'b' compareTo 'a': 2
str1 compareTo str3: -13
str1 compareTo str2: 0
str1 start with 'Hello': true
str1 ends with 'Java!': true
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
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"));
}
인덱스 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
public static void main(String[] args) {
String strWithSpaces = " Java Programming ";
System.out.println("소문자로 변환: " + strWithSpaces.toLowerCase());
System.out.println("대문자로 변환: " + strWithSpaces.toUpperCase());
System.out.println("공백 제거(trim): " + strWithSpaces.trim());
System.out.println("공백 제거(strip): " + strWithSpaces.strip());
System.out.println("공백 제거(strip): " + strWithSpaces.stripLeading());
System.out.println("공백 제거(strip): " + strWithSpaces.stripTrailing());
}
소문자로 변환: java programming
대문자로 변환: JAVA PROGRAMMING
공백 제거(trim): Java Programming
공백 제거(strip): Java Programming
공백 제거(strip): Java Programming
공백 제거(strip): Java Programming
public static void main(String[] args) {
String str = "Apple,Banana,Orange";
String[] split = str.split(",");
for (String s : split) {
System.out.println(s);
}
String joinStr = "";
for (String s : split) {
joinStr += s + "-";
}
joinStr ="";
for(int i = 0; i < split.length; i++){
String string = split[i];
joinStr += string;
if(i != split.length-1){
joinStr += "-";
}
}
System.out.println("joinStr = " + joinStr);
joinStr = String.join("-","A","B","C");
System.out.println("연결된 문자열 = " + joinStr);
String result = String.join("-", split);
System.out.println("result = "+result);
}
Apple
Banana
Orange
joinStr = Apple-Banana-Orange
연결된 문자열 = A-B-C
result = Apple-Banana-Orange
public static void main(String[] args) {
int num = 100;
boolean bool = true;
Object obj = new Object();
String str = "Hello, Java!";
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);
//toCharArray 메서드
char[] strCharArray = str.toCharArray();
for (char c : strCharArray) {
System.out.print(c);
}
}
숫자의 문자열 값: 100
불리언의 문자열 값: true
객체의 문자열 값: java.lang.Object@4e50df2e
빈문자열 + num: 100
Hello, Java!
public static void main(String[] args) {
int num = 100;
boolean bool = true;
String str = "Hello, Java!";
String formant1 = String.format("num: %d, bool: %b, str: %s", num, bool, str);
System.out.println(formant1);
String format2 = String.format("숫자: %.2f", 10.1234);
System.out.println(format2);
System.out.printf("숫자: %.2f\n",10.12345);
String regex = "Hello, (Java!|World)";
System.out.println("'str'이 패턴과 일치하는가?" + str.matches(regex));
}
num: 100, bool: true, str: Hello, Java!
숫자: 10.12
숫자: 10.12
'str'이 패턴과 일치하는가?true
참 많다.... 외우지는 말고 참고만 하도록 하자.
StringBuilder - 가변 String
불변인 String 클래스에도 단점이 있다.
불변인 String의 내부 값은 변경할 수 없다. 따라서 변경된 값을 기반으로 새로운 String 객체를 생성해야한다.
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점이다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC 해야 한다. 결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 된다. 그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모한다.
이 문제를 해결하는 방법은 String이 불변이 아닌 가변String을 제공한다. 물론 가변의 경우 "사이드 이펙트에 주의"해서 사용해야 한다.
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("reverse = " + sb);
//불변이 아니다. sb의 상태는 계속 변화하고 있다.
//StringBuilder -> String
String string = sb.toString();
System.out.println("string = " + string);
}
append() 메서드를 사용해 여러 문자열을 추가한다.
insert() 메서드로 특정 위치에 문자열을 삽입한다.
delete() 메서드로 특정 범위의 문자열을 삭제한다.
reverse() 메서드로 문자열을 뒤집는다.
마지막으로 toString 메서드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환한다.
**sb는 불변객체가 아니다.따라서 sb의 상태가 계속해서 변화하고 있음을 알아야한다.
가변(Mutable) vs 불변(Immutable)
String은 불변하다. 즉, 한 번 생성되면 그 내용을 변경할 수 없다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.
반면 StringBuilder는 가변적이기에 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이 때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있다. "사이드 이펙트가 발생할 수 있으니 주의해서 사용한다"
StringBuilder는 보통 문자열을 변경하는 동안 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋다.
String 최적화
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐준다.
컴파일 전
String helloWorld = "Hello" + " World!";
컴파일 후
String helloWorld = "Hello World!";
문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 순 없다.
String result = str1 + str2;
이런 경우 예를 들면 다음과 같이 최적화를 수행한다.
String result = new StringBuilder().append(str1).append(str2).toString();
이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 된다. 대신에 문자열을 더하기 연산(+)을 사용하면 충분하다.
public static void main(String[] args) {
long stratTime = 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 - stratTime));
}
Time = 6765
다음과 같은 예제의 경우 구닥다리 컴퓨터 기준 약 6.7초의 컴파일 시간이 걸렸다.
반복문 루프 내부에서는 최적화가 되는 것처럼 보이지만, 반복횟수만큼 객체를 생성해야 했기 때문이다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정된다. 이런 경우 최적화가 어렵다.
이러한 경우 StringBuilder를 사용해주자.
public static void main(String[] args) {
long stratTime = 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 - stratTime));
}
Time = 8
다음과 같은 경우 0.008초가 걸렸다.
문자열을 합칠때 대부분의 경우 최적화가 되므로 +를 사용하면 되지만, 반복문, 조건문, 복잡한 문자열의 특정 부분을 변경할 시, 매우 긴 대용량 문자열을 다룰 시 등 이러한 경우에 StringBuilder를 사용하도록 한다.
메서드 체인닝 - Method 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)의 참조값을 반환한다."
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder.add(1);
ValueAdder adder3 = adder.add(1);
int result = adder3.getValue();
System.out.println("result = " + result);
System.out.println("result = " + adder);
System.out.println("result = " + adder1);
System.out.println("result = " + adder2);
System.out.println("result = " + adder3);
}
adder, adder1, adder2, adder3의 참조값을 찍어보면 넷 다 동일한 것을 알 수 있다.
adder의 add에 return값이 자기 자신이기에 자신의 참조값을 return해줬기 때문이다.
그렇기때문에 위의 코드를 아래와 같이 정의할 수 있다.
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);
}
참조값이 동일하기 때문에
adder.add(1).add(2).add(3).getValue();
x001.add(1).add(2).add(3).getValue();
x001.add(2).add(3).getValue();
x001.add(3).getValue();
x001.getValue();
이 되기 때문이다.
이렇게 체인처럼 메서드가 연결되어있기 때문에 메서드체인닝이라고 불린다.
메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있다. 코드를 보면 . 을 찍고 메서드를 계속해서 연결한다.
기존에는 메서드를 호출할 때 마다 계속 변수명에 . 을 찍어야했다.
메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 . 을 찍어서 변수명을 생략 수 있다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문이다.
**StringBuilder에서는 메서드 체이닝 기법을 제공한다.
StringBuilder의 append()메서드를 보면 자기 자신의 참조값을 반환한다.
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("reverse = " + sb);
//불변이 아니다. sb의 상태는 계속 변화하고 있다.
//StringBuilder -> String
String string = sb.toString();
System.out.println("string = " + string);
}
이전에 작성했던 코드를 메서드 체이닝을 사용하여
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);
}
이렇게 작성할 수 있다.
"만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다"
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용한다.
'공부 > Java' 카테고리의 다른 글
자바 열거형-ENUM (3) | 2024.09.11 |
---|---|
자바 래퍼 클래스(wrapper class) (0) | 2024.09.10 |
자바 불변 객체 (0) | 2024.09.09 |
자바 Object 클래스 (1) | 2024.09.08 |
자바 다형성 3편 (Polymorphism) (1) | 2024.09.05 |