이 기사는 "Java 문자열 연결의 모범 사례?"라는 질문에서 나온 것입니다.
+ 연산자, StringBuilder.append 메서드 등 Java에서 문자열을 연결하는 방법에는 여러 가지가 있습니다. 및 각 방법의 단점(적절할 수 있음) 다양한 방법의 구현 세부 사항을 설명합니까?
효율성의 원칙에 따라 Java에서 문자열 연결에 대한 모범 사례는 무엇입니까?
문자열 처리에 대한 다른 모범 사례는 무엇입니까? 모범 사례는 무엇입니까?
더 이상 고민하지 말고 시작해 보겠습니다.
JDK 버전: 1.8.0_65
CPU: i7 4790
메모리: 16G
+접속 직접 사용
다음 코드를 보세요.
@Test public void test() { String str1 = "abc"; String str2 = "def"; logger.debug(str1 + str2); }
위 코드에서, 더하기 기호를 사용하여 네 개의 문자열을 연결합니다. 문자열 접합 방법의 장점은 분명합니다. 코드는 간단하고 직관적이지만 StringBuilder 및 StringBuffer와 비교하면 대부분의 경우 후자보다 낮습니다. 대부분의 경우 javap 도구를 사용하여 위 코드에서 생성된 바이트코드를 수행하고 컴파일러가 이 코드에 어떤 작업을 수행했는지 확인합니다.
public void test(); Code: 0: ldc #5 // String abc 2: astore_1 3: ldc #6 // String def 5: astore_2 6: aload_0 7: getfield #4 // Field logger:Lorg/slf4j/Logger; 10: new #7 // class java/lang/StringBuilder 13: dup 14: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V 17: aload_1 18: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: aload_2 22: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 25: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 28: invokeinterface #11, 2 // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;)V 33: return
디컴파일 결과에 따르면 + 연산자는 실제로 문자열을 연결하는 데 사용됩니다. 컴파일러는 컴파일 단계에서 StringBuilder 클래스를 사용하도록 코드를 최적화하고 문자열을 연결하기 위해 추가 메서드를 호출합니다. 스플라이싱하고 마지막으로 toString 메서드를 호출하면 일반적으로 +가 직접 사용된다고 볼 수 있나요? 어쨌든 컴파일러가 StringBuilder를 사용하도록 최적화하는 데 도움이 될까요?
StringBuilder 소스 코드 분석
답변 물론 불가능합니다. 그 이유는 StringBuilder 클래스가 내부적으로 수행하는 작업에 있습니다.
StringBuilder 클래스의 생성자를 살펴보겠습니다.
public StringBuilder() { super(16); } public StringBuilder(int capacity) { super(capacity); } public StringBuilder(String str) { super(str.length() + 16); append(str); } public StringBuilder(CharSequence seq) { this(seq.length() + 16); append(seq); }
StringBuilder는 매개변수 없는 생성자 외에도 3개의 다른 오버로드된 버전을 제공합니다. 상위 클래스의 super(int 용량) 구성 메소드는 내부적으로 호출되며 상위 클래스는 AbstractStringBuilder입니다. 구성 메소드는 다음과 같습니다.
AbstractStringBuilder(int capacity) { value = new char[capacity]; }
StringBuilder는 실제로 내부적으로 char 배열을 사용하는 것을 볼 수 있습니다. . Data(String 및 StringBuffer도 포함). 여기서 용량 값은 배열의 크기를 지정합니다. StringBuilder의 매개변수 없는 생성자와 결합하면 기본 크기가 16자임을 알 수 있습니다.
즉, 접합할 문자열의 전체 길이가 16자 이상인 경우 직접 접합과 수동 StringBuilder 사이에는 큰 차이가 없지만 크기를 지정할 수 있습니다. 너무 많은 메모리 할당을 피하기 위해 StringBuilder 클래스를 직접 구성하여 배열을 만듭니다.
이제 StringBuilder.append 메서드 내에서 수행되는 작업을 살펴보겠습니다.
@Override public StringBuilder append(String str) { super.append(str); return this; }
직접 호출되는 상위 클래스의 추가 메서드:
public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }
메서드 내에서 verifyCapacityInternal 메소드가 호출됩니다. 스플라이싱된 문자열의 전체 크기가 내부 배열 값의 크기보다 큰 경우 스플라이싱하기 전에 확장해야 합니다.
void expandCapacity(int minimumCapacity) { int newCapacity = value.length * 2 + 2; if (newCapacity - minimumCapacity < 0) newCapacity = minimumCapacity; if (newCapacity < 0) { if (minimumCapacity < 0) // overflow throw new OutOfMemoryError(); newCapacity = Integer.MAX_VALUE; } value = Arrays.copyOf(value, newCapacity); }
StringBuilder는 확장시 용량을 추가합니다. 현재 용량의 2배 + 2로 용량을 늘리는 것은 정말 끔찍한 일입니다. 구축 시 용량을 지정하지 않으면 많은 양의 메모리 공간이 확보될 가능성이 매우 높습니다. 확장 이후 점유 및 낭비됨. 둘째, 확장 후 Arrays.copyOf 메소드를 호출하는데, 이 메소드는 확장 전의 데이터를 확장된 공간에 복사합니다. 그 이유는 StringBuilder는 내부적으로 char 배열을 사용하여 데이터를 확장할 수 없기 때문입니다. 메모리 공간을 다시 적용하고 기존 데이터를 새 공간에 복사합니다. 여기서는 마지막으로 System.arraycopy 메소드를 호출하여 복사합니다. 이는 기본 메소드이므로 메모리를 사용하는 것보다 낫습니다. 그럼에도 불구하고 많은 양의 메모리 공간을 적용하고 데이터를 복사하는 영향을 무시할 수 없습니다.
+스플라이싱 사용과 StringBuilder 사용 비교
@Test public void test() { String str = ""; for (int i = 0; i < 10000; i++) { str += "asjdkla"; } }
위 코드는 다음과 동일하도록 최적화되었습니다.
@Test public void test() { String str = null; for (int i = 0; i < 10000; i++) { str = new StringBuilder().append(str).append("asjdkla").toString(); } }
그것도 한 눈에 알 수 있습니다. 많은 StringBuilder 객체가 생성되고 str은 각 사이클마다 점점 더 커지므로 매번 적용되는 메모리 공간이 점점 커지고 str의 길이가 16보다 큰 경우 매번 두 번 확장해야 합니다! 실제로 toString 메소드는 String 객체를 생성할 때 Arrays.copyOfRange 메소드를 호출하여 데이터를 복사하는데, 이는 실행될 때마다 용량을 2번 확장하고 데이터를 3번 복사하는 것과 같습니다.
public void test() { StringBuilder sb = new StringBuilder("asjdkla".length() * 10000); for (int i = 0; i < 10000; i++) { sb.append("asjdkla"); } String str = sb.toString(); }
내 컴퓨터에서 이 코드의 실행 시간은 0ms(1ms 미만)와 1ms인 반면, 위 코드는 약 380ms입니다. 효율성의 차이는 매우 분명합니다.
위와 같은 코드에서 루프 수를 1000000으로 조정하면 내 컴퓨터에서는 용량을 지정했을 때 약 20ms, 용량을 지정하지 않았을 때는 약 29ms 정도 걸리는데 사용하는 것과는 차이가 있다. 직접 + 연산자가 크게 개선되었지만(루프 수가 100배 증가함) 여전히 여러 확장 및 복사를 트리거합니다.
위 코드를 StringBuffer를 사용하도록 변경합니다. 내 컴퓨터에서는 약 33ms가 소요됩니다. 이는 StringBuffer가 스레드 안전성과 실행 효율성을 보장하기 위해 대부분의 메서드에 동기화된 키워드를 추가하기 때문입니다.
String.concat을 사용하여 연결
이제 다음 코드를 살펴보세요.
@Test public void test() { String str = ""; for (int i = 0; i < 10000; i++) { str.concat("asjdkla"); } }
这段代码使用了String.concat方法,在我的机器上,执行时间大约为130ms,虽然直接相加要好的多,但是比起使用StringBuilder还要太多了,似乎没什么用。其实并不是,在很多时候,我们只需要连接两个字符串,而不是多个字符串的拼接,这个时候使用String.concat方法比StringBuilder要简洁且效率要高。
public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); }
上面这段是String.concat的源码,在这个方法中,调用了一次Arrays.copyOf,并且指定了len + otherLen,相当于分配了一次内存空间,并分别从str1和str2各复制一次数据。而如果使用StringBuilder并指定capacity,相当于分配一次内存空间,并分别从str1和str2各复制一次数据,最后因为调用了toString方法,又复制了一次数据。
结论
现在根据上面的分析和测试可以知道:
Java中字符串拼接不要直接使用+拼接。
使用StringBuilder或者StringBuffer时,尽可能准确地估算capacity,并在构造时指定,避免内存浪费和频繁的扩容及复制。
在没有线程安全问题时使用StringBuilder, 否则使用StringBuffer。
两个字符串拼接直接调用String.concat性能最好。
关于String的其他最佳实践
用equals时总是把能确定不为空的变量写在左边,如使用"".equals(str)判断空串,避免空指针异常。
第二点是用来排挤第一点的.. 使用str != null && str.length() != 0来判断空串,效率比第一点高。
在需要把其他对象转换为字符串对象时,使用String.valueOf(obj)而不是直接调用obj.toString()方法,因为前者已经对空值进行检测了,不会抛出空指针异常。
使用String.format()方法对字符串进行格式化输出。
在JDK 7及以上版本,可以在switch结构中使用字符串了,所以对于较多的比较,使用switch代替if-else。