> 类库下载 > java类库 > Java 문자열 접합 효율성 분석 및 모범 사례

Java 문자열 접합 효율성 분석 및 모범 사례

高洛峰
풀어 주다: 2016-10-13 09:23:05
원래의
1859명이 탐색했습니다.

이 기사는 "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。


원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿