평일에는 프로젝트의 로직 구현으로 바빠서 토요일에 시간이 좀 있어서 책장에서 두꺼운 영문판 Thinking In Java를 꺼내 스트링 객체의 스플라이싱에 대해 읽어보았습니다. 이 책을 참고하여 번역하고, 자신의 생각을 추가하고, 이를 기록하기 위해 이 글을 쓰세요.
불변 문자열 객체
Java에서 String 객체는 변경할 수 없습니다. 코드에서는 String 개체에 대한 여러 별칭을 만들 수 있습니다. 그러나 이러한 별칭은 모두 동일한 것을 나타냅니다.
예를 들어, s1과 s2는 모두 "droidyue.com" 개체의 별칭이며 별칭은 실제 개체에 대한 참조를 저장합니다. 따라서 s1 = s2
다음과 같이 코드 코드를 복사합니다.
문자열 s1 = "droidyue.com";
문자열 s2 = s1;
System.out.println("s1과 s2는 동일한 참조를 가집니다. =" + (s1 == s2));
Java의 유일한 오버로드된 연산자
Java에서 오버로드된 유일한 연산자는 문자열 연결과 관련되어 있습니다. +,+=. 또한 Java 디자이너는 다른 연산자의 오버로드를 허용하지 않습니다.
접합 분석
실제로 성능 비용이 있습니까?
위의 두 가지 사항을 이해한 후에는 Sting 객체가 불변이기 때문에 여러(3개 이상의) 문자열을 연결하면 필연적으로 중복되는 중간 String 객체가 생성된다는 생각이 들 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
String userName = "앤디";
문자열 연령 = "24";
String job = "개발자";
문자열 정보 = 사용자 이름 + 나이 + 직업;
위의 정보를 얻으려면 userName과 age를 연결하여 임시 문자열 개체 t1을 생성하고, 내용은 Andy24이며, 그런 다음 t1과 job을 연결하여 필요한 최종 정보 개체를 생성합니다. 중간 t1이 생성되고 t1이 생성됩니다. 이후 적극적인 재활용이 없으면 필연적으로 일정량의 공간을 차지하게 됩니다. 많은 문자열(주로 개체의 toString 호출에서 수백 개 가정)을 연결하는 경우 비용은 훨씬 더 커지고 성능은 많이 저하됩니다.
컴파일러 최적화 처리
실제로 위와 같은 성능 비용이 있습니까? 일반적으로 사용되는 문자열 연결에 대한 특별한 처리 최적화가 없습니까? 대답은 '예'입니다. 이 최적화는 컴파일러가 .java를 바이트코드로 컴파일할 때 수행됩니다.
Java 프로그램을 실행하려면 컴파일 시간과 런타임이라는 두 가지 기간을 거쳐야 합니다. 컴파일하는 동안 Java 컴파일러(컴파일러)는 Java 파일을 바이트코드로 변환합니다. 런타임 시 JVM(Java Virtual Machine)은 컴파일 시 생성된 바이트코드를 실행합니다. 이 두 기간을 통해 Java는 소위 컴파일을 한 곳에서 달성하고 모든 곳에서 실행됩니다.
컴파일 중에 어떤 최적화가 이루어졌는지 실험해 보고 성능이 저하될 수 있는 코드 조각을 만들 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 연결 {
공개 정적 무효 메인(String[] args) {
String userName = "앤디";
문자열 연령 = "24";
String job = "개발자";
문자열 정보 = 사용자 이름 + 나이 + 직업;
System.out.println(정보);
}
}
Concatenation.java를 컴파일합니다. getConcatenation.class
다음과 같이 코드 코드를 복사합니다.
javacConcatenation.java
그런 다음 javap를 사용하여 컴파일된 Concatenation.class 파일을 디컴파일합니다. javap -c 연결. javap 명령을 찾을 수 없는 경우 javap가 있는 디렉터리를 환경 변수에 추가하거나 javap의 전체 경로를 사용하는 것이 좋습니다.
다음과 같이 코드 코드를 복사합니다.
17:22:04-androidyue~/workspace_adt/strings/src$ javap -c 연결
"Concatenation.java"에서 컴파일됨
공개 클래스 연결 {
공개 연결();
암호:
0: 로드_0
1: Invokespecial #1 // 메소드 java/lang/Object."<init>":()V
4: 복귀
공개 정적 무효 메인(java.lang.String[]);
암호:
0: ldc #2 // 문자열 앤디
2: astore_1
3: ldc #3 // 문자열 24
5: astore_2
6: ldc #4 // 문자열 개발자
8: astore_3
9: 새로운 #5 // 클래스 java/lang/StringBuilder
12: 복제
13: Invokespecial #6 // 메소드 java/lang/StringBuilder."<init>":()V
16: 로드_1
17: Invokevirtual #7 // 메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: 로드_2
21: Invokevirtual #7 // 메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: 로드_3
25: Invokevirtual #7 // 메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: Invokevirtual #8 // 메소드 java/lang/StringBuilder.toString:()Ljava/lang/String;
31 : 매장 4
33: getstatic #9 // 필드 java/lang/System.out:Ljava/io/PrintStream;
36 : 부하 4
38: Invokevirtual #10 // 메소드 java/io/PrintStream.println:(Ljava/lang/String;)V
41: 복귀
}
그 중 ldc, astore 등은 어셈블리 명령어와 유사한 Java 바이트코드 명령어이다. 다음 설명에서는 설명을 위해 Java 관련 내용을 사용합니다. 위에서는 많은 StringBuilder가 있음을 알 수 있지만 Java 코드에서 이를 명시적으로 호출하지는 않습니다. 이는 Java 컴파일러가 문자열 접합을 발견하면 StringBuilder 객체를 생성하고 다음을 수행합니다. 접합은 실제로 StringBuilder 개체의 추가 메서드를 호출합니다. 이렇게 하면 위에서 걱정했던 문제가 발생하지 않습니다.
컴파일러 최적화만?
컴파일러가 우리를 위해 최적화를 수행했기 때문에 컴파일러의 최적화에만 의존하는 것으로 충분합니까? 물론 아닙니다.
아래에서는 성능이 낮은 최적화되지 않은 코드를 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
공개 무효 암시적UseStringBuilder(String[] 값) {
문자열 결과 = "";
for (int i = 0; i < value.length; i ++) {
결과 += 값[i];
}
System.out.println(결과);
}
javac를 사용하여 컴파일하고 javap를 사용하여 확인하세요.
다음과 같이 코드 코드를 복사합니다.
공개 무효 암시적UseStringBuilder(java.lang.String[]);
암호:
0: ldc #11 // 문자열
2: astore_2
3: 아이콘t_0
4: istore_3
5: iload_3
6: 로드_1
7: 배열 길이
8: if_icmpge 38
11: 새로운 #5 // 클래스 java/lang/StringBuilder
14: 복제
15: Invokespecial #6 // 메소드 java/lang/StringBuilder."<init>":()V
18: 로드_2
19: Invokevirtual #7 // 메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: 로드_1
23: iload_3
24: 로드하다
25: Invokevirtual #7 // 메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: Invokevirtual #8 // 메소드 java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_2
32: iinc 3, 1
35: 5로 이동
38: getstatic #9 // 필드 java/lang/System.out:Ljava/io/PrintStream;
41: 로드_2
42: Invokevirtual #10 // 메소드 java/io/PrintStream.println:(Ljava/lang/String;)V
45: 복귀
그중 8: if_icmpge 38 및 35: goto 5가 루프를 형성합니다. 8: if_icmpge 38은 JVM 피연산자 스택의 정수 비교가 (i <values.length의 반대 결과)보다 크거나 같은 경우 라인 38(System.out)로 점프함을 의미합니다. 35: goto 5는 5번째 줄로 바로 점프한다는 뜻입니다.
그러나 여기서 매우 중요한 점 중 하나는 StringBuilder 개체 생성이 루프 사이에서 발생한다는 것입니다. 이는 많은 루프에서 얼마나 많은 StringBuilder 개체가 생성되는지를 의미하며 이는 분명히 좋지 않습니다. 노출된 저수준 코드.
약간의 최적화만으로도 성능이 즉시 향상될 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
공공 무효explicitUseStringBuider(String[] 값) {
StringBuilder 결과 = 새로운 StringBuilder();
for (int i = 0; i < value.length; i ++) {
result.append(값[i]);
}
}
해당 컴파일된 정보
다음과 같이 코드 코드를 복사합니다.
공공 무효 명시적UseStringBuider(java.lang.String[]);
암호:
0: 새로운 #5 // 클래스 java/lang/StringBuilder
3: 복제
4: Invokespecial #6 // 메소드 java/lang/StringBuilder."<init>":()V
7: astore_2
8: 아이콘st_0
9: istore_3
10: iload_3
11: 로드_1
12:배열 길이
13: if_icmpge 30
16: 로드_2
17: 로드_1
18: iload_3
19: 로드하다
20: Invokevirtual #7 // 메소드 java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: 팝
24: iinc 3, 1
27: 10으로 이동
30: 복귀
위에서 알 수 있듯이 13: if_icmpge 30과 27: goto 10은 루프 루프를 형성하고, 0: new #5는 루프 외부에 위치하므로 StringBuilder가 여러 번 생성되지 않습니다.
일반적으로 루프 본문에 StringBuilder를 암시적으로 또는 명시적으로 생성하는 것을 피해야 합니다. 따라서 코드가 어떻게 컴파일되고 내부적으로 실행되는지 이해하는 사람은 더 높은 수준의 코드를 작성할 수 있습니다.
위 글에 오류가 있으면 비판하고 정정해 주시기 바랍니다.