@Test
public void 문자열_비교() {
// when
String hello = "hello";
String hello2 = "hello";
//TODO 이 방법은 권장하지 않습니다.
String hello3 = new String("hello");
//then
assertThat(hello == hello3).isFalse();
assertThat(hello.equals(hello2)).isTrue();
assertThat(hello == hello2).isTrue();
assertThat(hello == hello2).isTrue();
}
동일한 기능을 함에도 여러번 생성하는 경우들이 있는데 하나씩 알아보자
1️⃣ 문자열
📌 문자열 생성 방법
String hello1 = "hello";
String hello2 = new String("hello"); //이 방법은 권장하지 않습니다.
😮💨 권장하지 않는 방법을 사용했을 때 문제점
1. String 생성자로 들어가는 "hello" 자체가 이 생성자로 만들어내려는 String과 기능적으로 완전히 똑같다.
2. Java에서 문자열 리터럴을 저장하는 독립된 영역인 String Constant Pool를 이용한 재사용을 하지 못한다.
💬 2번 문제를 조금 더 보자
String은 불변 객체로 String Constant Pool를 통해서 성능, 동기화, 캐싱, 보안의 이점을 얻고 있는데 new로 생성하면 heap 영역에 저장되기 때문에 활용하지 못한다.
- 궁금하면 여기를 참고하세요
테스트
@Test
public void 문자열_비교() {
// when
String hello = "hello";
String hello2 = "hello";
//TODO 이 방법은 권장하지 않습니다.
String hello3 = new String("hello");
//then
assertThat(hello == hello3).isFalse();
assertThat(hello.equals(hello2)).isTrue();
assertThat(hello == hello2).isTrue();
assertThat(hello == hello2).isTrue();
}
2️⃣ 정규표현식
정규표현식용 Pattern 인스턴스는 생성 비용이 높고 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다.
public boolean isPhoneNumber(String phoneNumber) {
return phoneNumber.matches("^\\d{3}-\\d{3,4}-\\d{4}$");
}
정적 초기화를 통해서 직접 생성해 캐싱해두면 재사용시 성능의 이점을 얻을 수 있다.
- static final 필드로 추출하면서 이름도 얻을 수 있게 때문에 무슨 정규표현식인지 이해하기 쉽다.
public class Main {
private static final Pattern PHONE_NUMBER_PATTERN =
Pattern.compile("^\\d{3}-\\d{3,4}-\\d{4}$");
public boolean isPhoneNumberCache(String phoneNumber) {
return PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches();
}
public static void main(String[] args) {
Main main = new Main();
List<String> phoneNumbers = IntStream.rangeClosed(1, 1000)
.mapToObj(s -> "010-1234-5678")
.collect(Collectors.toList());
long start = System.nanoTime();
List<String> result = phoneNumbers.stream()
.filter(main::isPhoneNumberCache)
.collect(Collectors.toList());
long end = System.nanoTime();
System.out.println(end - start);
}
}
😛 정규 표현식을 matches말고 split과 replace에서도 사용된다.
String.split(String regex)
String의 split 메서드 주석을 보면 아래 2가지 중에서 포함되면 내부적인 최적화를 얻을 수 있다.
- 길이가 한 글자인 문자열로, 이 문자열이 정규 표현식에서 메타 문자(meta characters)인 ".$|()[{^?*+\" 중 하나가 아닌 경우
- 길이가 두 글자인 문자열로, 첫 번째 글자가 백슬래시(\\)이고, 두 번째 글자가 아스키(ASCII) 숫자나 알파벳 문자가 아닌 경우
위에 경우가 아니면 정규 표현식으로 최적화 하면 된다.
Pattern.compile(regex).split(str)
❗️주의할점
split 최적화가 가능한 경우 Pattern 객체를 캐시처리하는 것보다 빠르게 나왔다.
1️⃣ split 최적화 - 2500000 ~ 2800000
public static void main(String[] args) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
String name = "야호,크크,메롱,웃겨?";
name.split(",");
}
System.out.println(System.nanoTime() - start); // 2587458
}
2️⃣ Pattern 객체 캐시해서 사용- 4300000 ~ 4800000
public class SplitCache {
private static final Pattern SPLIT_PATTERN = Pattern.compile(",");
public static void main(String[] args) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
String name = "야호,크크,메롱,웃겨?";
SPLIT_PATTERN.split(name);
}
System.out.println(System.nanoTime() - start); //4816792
}
}
String.replace(String regex, String replacement)
Pattern.compile(regex).matcher(str).replaceAll(repl)
3️⃣ 오토박싱(auto boxing)
오토박싱: 프리미티브 타입을 wrapper 타입으로 변경시 발생
언박싱: wrapper 타입을 프리미티브 타입으로 변환하는 과정
❗️오토박싱의 문제점
오토박싱, 언 박싱 을 런타임에 JVM 이 자동으로 처리하는데 불필요하게 오토박싱이 일어나는 경우 Wrapper 클래스의 인스턴스가 계속 만들어지게 된다.
public class Sum {
private static long sum() {
Long sum = 0L; // long이 아니라 Long이다.
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i; // 오토박싱 발생
return sum;
}
public static void main(String[] args) {
long start = System.nanoTime();
long x = sum();
long end = System.nanoTime();
System.out.println((end - start) / 1_000_000. + " ms.");
System.out.println(x);
}
}
오토박싱이 일어나지 않게 수정하면 성능이 개선된다.
Long sum = 0L; ➡️ long sum = 0L;
📚 Reference
'책 > 이펙티브 자바' 카테고리의 다른 글
item3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2024.02.18 |
---|---|
item2-생성자에 매개변수가 많다면 빌더를 고려하자 (0) | 2024.02.16 |
Item1 - 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2024.02.15 |