과거를 복습하고 새로운 것을 배운다 (1) Java의 문자열에 대한 심층적 이해
관련 학습 권장 사항: Java 기본 튜토리얼
Java를 처음 배웠을 때 우리는 Java가 두 가지 주요 데이터 유형, 즉 기본 데이터 유형과 참조 데이터 유형으로 나눌 수 있다는 것을 이미 알고 있었습니다. 이 두 가지 데이터 유형 중에는 특별한 데이터 유형인 String이 있습니다. String은 참조 데이터 유형이지만 다른 참조 데이터 유형과 다릅니다. 데이터형 중에서는 특이한 꽃이라고 할 수 있다. 따라서 이 기사에서는 Java의 문자열 문자열에 대해 심층적으로 이해하겠습니다.
1. 문자열 문자열의 메모리 할당부터 시작하겠습니다
이전 글 "과거를 복습하고 새로운 것을 배우기 - 당신이 모르는 JVM 메모리 할당"에서는 JVM 메모리 모델을 자세히 분석했습니다. 상수 풀 섹션에서는 문자열 상수 풀, 클래스 파일 상수 풀, 런타임 상수 풀이라는 세 가지 유형의 상수 풀에 대해 배웠습니다. 문자열의 메모리 할당은 문자열 상수 풀과 큰 관계가 있습니다.
문자열을 인스턴스화하는 방법에는 두 가지가 있습니다. 가장 일반적으로 사용되는 첫 번째 방법은 리터럴 할당을 이용하는 것이고, 다른 하나는 매개변수를 전달하는 구성 방법을 이용하는 것입니다. 코드는 다음과 같습니다.
String str1="abc"; String str2=new String("abc");复制代码
메모리 할당에서 이 두 가지 방법의 차이점은 무엇입니까? 선생님이 Java를 처음 배웠을 때 설명하신 것 같습니다.
1 리터럴 할당을 통해 문자열을 생성합니다. 문자열 상수 풀의 개체입니다. 2. 생성자를 통해 String 매개변수를 전달하면 힙 메모리와 문자열 상수 풀에 String 개체가 생성되고 힙 메모리의 String 참조가 스택에 저장됩니다.
이 답변이 맞나요? 사용되는 Java 버전에 전적으로 의존하기 때문에 적어도 지금은 완전히 정확하지 않은 것 같습니다. 이전 기사 "과거를 검토하고 새로운 것을 배우십시오 - 당신이 모르는 JVM 메모리 할당"에서는 HotSpot 가상 머신의 다양한 JDK에서의 문자열 상수 풀 구현에 대해 설명했습니다. 발췌 내용은 다음과 같습니다.
JDK7 이전에는 문자열이 있습니다. 상수 풀은 메소드 영역(영구 생성)에 있으며 이때 상수 풀은 문자열 객체를 저장합니다. JDK7에서는 문자열 상수 풀이 메서드 영역에서 힙 메모리로 이동되고 문자열 개체는 Java 힙에 저장됩니다. 문자열 상수 풀은 문자열 개체에 대한 참조만 저장합니다.
이 문장을 어떻게 이해해야 할까요? 분석을 위해 String str1=new String("abc")을 예로 들어 보겠습니다.
1. JDK6의 메모리 할당
먼저 아래 그림과 같이 JDK6의 메모리 할당을 분석해 보겠습니다.


String의 메모리 할당을 이해한 후에는 String에서 매우 중요한 메소드인 String.intern()을 알아야 합니다.
많은 독자들이 이 방법에 대해 잘 알지 못할 수도 있지만 이것이 중요하지 않다는 의미는 아닙니다. 먼저 intern() 메서드의 소스 코드를 살펴보겠습니다./**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();复制代码
로그인 후 복사
emmmm.... 실제로는 네이티브 메서드이지만 소스 코드를 볼 수 없더라도 상관은 없습니다. 주석에서 정보를 얻으세요: /** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern();复制代码
intern 메소드를 호출할 때 문자열 상수 풀에 이미 String 개체와 동일한 문자열이 포함되어 있으면 문자열 상수 풀의 문자열에 대한 참조가 직접 반환됩니다. 그렇지 않으면 이 문자열 개체에 포함된 문자열이 상수 풀에 추가되고 이 개체에 대한 참조가 반환됩니다.
1.一个关于intern()的简单例子
了解了intern方法的用途之后,来看一个简单的列子:
public class Test { public static void main(String[] args) { String str1 = "hello world"; String str2 = new String("hello world"); String str3=str2.intern(); System.out.println("str1 == str2:"+(str1 == str2)); System.out.println("str1 == str3:"+(str1 == str3)); } }复制代码
上面的一段代码会输出什么?编译运行之后如下:

如果理解了intern方法就很容易解释这个结果了,从上面截图中可以看到,我们的运行环境是JDK8。
String str1 = "hello world"; 这行代码会首先在Java堆中创建一个对象,并将该对象的引用放入字符串常量池中,str1指向常量池中的引用。
String str2 = new String("hello world");这行代码会通过new来实例化一个String对象,并将该对象的引用赋值给str2,然后检测字符串常量池中是否已经有了与“hello world”相等的对象,如果没有,则会在堆内存中再生成一个值为"hello world"的对象,并将其引用放入到字符串常量池中,否则,不会再去创建。这里,第一行代码其实已经在字符串常量池中保存了“hello world”字符串对象的引用,因此,第二行代码就不会再次向常量池中添加“hello world"的引用。
String str3=str2.intern(); 这行代码会首先去检测字符串常量池中是否已经包含了”hello world"的String对象,如果有则直接返回其引用。而在这里,str2.intern()其实刚好返回了第一行代码中生成的“hello world"对象。
因此【System.out.println("str1 == str3:"+(str1 == str3));】这行代码会输出true.
如果切到JDK6,其打印结果与上一致,至于原因读者可以自行分析。

2.改造例子,再看intern
上一节中我们通过一个例子认识了intern()方法的作用,接下来,我们对上述例子做一些修改:
public class Test { public static void main(String[] args) { String str1=new String("he")+new String("llo"); String str2=str1.intern(); String str3="hello"; System.out.println("str1 == str2:"+(str1 == str2)); System.out.println("str2 == str3:"+(str2 == str3)); } }复制代码
先别急着看下方答案,思考一下在JDK7(或JDK7之后)及JDK6上会输出什么结果?
1).JDK8的运行结果分析
我们先来看下我们先来看下JDK8的运行结果:

通过运行程序发现输出的两个结果都是true,这是为什么呢?我们通过一个图来分析:

String str1=new String("he")+new String("llo"); 这行代码中new String("he")和new String("llo")会在堆上生成四个对象,因为与本例无关,所以图上没有画出,new String("he")+new String("llo")通过”+“号拼接后最终会生成一个"hello"对象并赋值给str1。
String str2=str1.intern(); 这行代码会首先检测字符串常量池,发现此时还没有存在与”hello"相等的字符串对象的引用,而在检测堆内存时发现堆中已经有了“hello"对象,遂将堆中的”hello"对象的应用放入字符串常量池中。
String str3="hello"; 这行代码发现字符串常量池中已经存在了“hello"对象的引用,因此将str3指向了字符串常量池中的引用。
此时,我们发现str1、str2、str3指向了堆中的同一个”hello"对象,因此,就有了上边两个均为true的输出结果。
2).JDK6的运行结果分析
我们将运行环境切换到JDK6,来看下其输出结果:

有点意思!相同的代码在不同的JDK版本上输出结果竟然不相等。这是怎么回事呢?我们还通过一张图来分析:

String str1=new String("he")+new String("llo"); 这行代码会通过new String("he")和new String("llo")会分别在Java堆与字符串常量池中各生成两个String对象,由于与本例无关,所以并没有在图中画出。而new String("he")+new String("llo")通过“+”号拼接后最终会在Java堆上生成一个"hello"对象,并将其赋值给了str1。
String str2=str1.intern(); 这行代码检测到字符串常量池中还没有“hello"对象,因此将堆中的”hello“对象复制到了字符串常量池,并将其赋值给str2。
String str3="hello"; 这行代码检测到字符串常量池中已经有了”hello“对象,因此直接将str3指向了字符串常量池中的”hello“对象。 此时str1指向的是Java堆中的”hello“对象,而str2和str3均指向了字符串常量池中的对象。因此,有了上面的输出结果。
通过这两个例子,相信大家因该对String的intern()方法有了较深的认识。那么intern()方法具体在开发中有什么用呢?推荐大家可以看下美团技术团队的一篇文章《深入解析String#intern》中举的两个例子。限于篇幅,本文不再举例分析。
三、String类的结构及特性分析
前两节我们认识了String的内存分配以及它的intern()方法,这两节内容其实都是对String内存的分析。到目前为止,我们还并未认识String类的结构以及它的一些特性。那么本节内容我们就此来分析。先通过一段代码来大致了解一下String类的结构(代码取自jdk8):
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 // ...}复制代码
可以看到String类实现了Serializable接口、Comparable接口以及CharSequence接口,意味着它可以被序列化,同时方便我们排序。另外,String类还被声明为了final类型,这意味着String类是不能被继承的。而在其内部维护了一个char数组,说明String是通过char数组来实现的,同时我们注意到这个char数组也被声明为了final,这也是我们常说的String是不可变的原因。
1.不同JDK版本之间String的差异
Java的设计团队一直在对String类进行优化,这就导致了不同jdk版本上String类的实现有些许差异,只是我们使用上并无感知。下图列出了jdk6-jdk9中String源码的一些变化。

可以看到在Java6之前String中维护了一个char 数组、一个偏移量 offset、一个字符数量 count以及一个哈希值 hash。 String对象是通过 offset 和 count 两个属性来定位 char[] 数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间,但这种方式很有可能会导致内存泄漏。
在Java7和Java8的版本中移除了 offset 和 count 两个变量了。这样的好处是String对象占用的内存稍微少了些,同时 String.substring 方法也不再共享 char[],从而解决了使用该方法可能导致的内存泄漏问题。
从Java9开始,String中的char数组被byte[]数组所替代。我们知道一个char类型占用两个字节,而byte占用一个字节。因此在存储单字节的String时,使用char数组会比byte数组少一个字节,但本质上并无任何差别。 另外,注意到在Java9的版本中多了一个coder,它是编码格式的标识,在计算字符串长度或者调用 indexOf() 函数时,需要根据这个字段,判断如何计算字符串长度。coder 属性默认有 0 和 1 两个值, 0 代表Latin-1(单字节编码),1 代表 UTF-16 编码。如果 String判断字符串只包含了 Latin-1,则 coder 属性值为 0 ,反之则为 1。
2.String字符串的裁剪、拼接等操作分析
在本节内容的开头我们已经知道了字符串的不可变性。那么为什么我们还可以使用String的substring方法进行裁剪,甚至可以直接使用”+“连接符进行字符串的拼接呢?
(1)String的substring实现
关于substring的实现,其实我们直接深入String的源码查看即可,源码如下:
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }复制代码
从这段代码中可以看出,其实字符串的裁剪是通过实例化了一个新的String对象来实现的。所以,如果在项目中存在大量的字符串裁剪的代码应尽量避免使用String,而是使用性能更好的StringBuilder或StringBuffer来处理。
(2)String的字符串拼接实现
1)字符串拼接方案性能对比
关于字符串的拼接有很多实现方法,在这里我们举三个例子来进行一个性能对比,分别如下:
使用”+“操作符拼接字符串
public class Test { private static final int COUNT=50000; public static void main(String[] args) { String str=""; for(int i=0;i<COUNT;i++) { str=str+"abc"; } }复制代码
使用String的concat()方法拼接
public class Test { private static final int COUNT=50000; public static void main(String[] args) { String str=""; for(int i=0;i<COUNT;i++) { str=str+"abc"; } }复制代码
使用StringBuilder的append方法拼接
public class Test { private static final int COUNT=50000; public static void main(String[] args) { StringBuilder str=new StringBuilder(); for(int i=0;i<COUNT;i++) { str.append("abc"); } }复制代码
如上代码,通过三种方法分别进行了50000次字符串拼接,每种方法分别运行了20次。统计耗时,得到以下表格:
拼接方法 | 最小用时(ms) | 最大用时(ms) | 平均用时(ms) |
---|---|---|---|
"+"操作符 | 4868 | 5146 | 4924 |
String的concat方法 | 2227 | 2456 | 2296 |
StringBuilder的append方法 | 4 | 12 | 6.6 |
从以上数据中可以很直观的看到”+“操作符的性能是最差的,平均用时达到了4924ms。其次是String的concat方法,平均用时也在2296ms。而表现最为优秀的是StringBuilder的append方法,它的平均用时竟然只有6.6ms。这也是为什么在开发中不建议使用”+“操作符进行字符串拼接的原因。
2)三种字符串拼接方案原理分析
”+“操作符的实现原理由于”+“操作符是由JVM来完成的,我么无法直接看到代码实现。不过Java为我们提供了一个javap的工具,可以帮助我们将Class文件进行一个反汇编,通过汇编指令,大致可以看出”+“操作符的实现原理。
public class Test { private static final int COUNT=50000; public static void main(String[] args) { for(int i=0;i<COUNT;i++) { str=str+"abc"; } }复制代码
把上边这段代码编译后,执行javap,得到如下结果:

注意图中的”11:“行指令处实例化了一个StringBuilder,在"19:"行处调用了StringBuilder的append方法,并在第”27:"行处调用了String的toString()方法。可见,JVM在进行”+“字符串拼接时也是用了StringBuilder来实现的,但为什么与直接使用StringBuilder的差距那么大呢?其实,只要我们将上边代码转换成虚拟机优化后的代码一看便知:
public class Test { private static final int COUNT=50000; public static void main(String[] args) { String str=""; for(int i=0;i<COUNT;i++) { str=new StringBuilder(str).append("abc").toString(); } }复制代码
可见,优化后的代码虽然也是用的StringBuilder,但是StringBuilder却是在循环中实例化的,这就意味着循环了50000次,创建了50000个StringBuilder对象,并且调用了50000次toString()方法。怪不得用了这么长时间!!!
String的concat方法的实现原理关于concat方法可以直接到String内部查看其源码,如下:
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); }复制代码
可以看到,在concat方法中使用Arrays的copyOf进行了一次数组拷贝,接下来又通过getChars方法再次进行了数组拷贝,最后通过new实例化了String对象并返回。这也意味着每调用一次concat都会生成一个String对象,但相比”+“操作符却省去了toString方法。因此,其性能要比”+“操作符好上不少。
至于StringBuilder其实也没必要再去分析了,毕竟”+“操作符也是基于StringBuilder实现的,只不过拼接过程中”+“操作符创建了大量的对象。而StringBuilder拼接时仅仅创建了一个StringBuilder对象。
四、总结
本篇文章我们深入分析了String字符串的内存分配、intern()方法,以及String类的结构及特性。关于这块知识,网上的文章鱼龙混杂,甚至众说纷纭。笔者也是参考了大量的文章并结合自己的理解来做的分析。但是,避免不了的可能会出现理解偏差的问题,如果有,希望大家多多讨论给予指正。 同时,文章中多次提到StringBuilder,但限于文章篇幅,没能给出关于其详细分析。不过不用担心,我会在下一篇文章中再做探讨。 不管怎样,相信大家看完这篇文章后一定 对String有了更加深入的认识,尤其是了解String类的一些裁剪及拼接中可能造成的性能问题,在今后的开发中应该尽量避免。
위 내용은 과거를 복습하고 새로운 것을 배운다 (1) Java의 문자열에 대한 심층적 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











Java의 Smith Number 가이드. 여기서는 정의, Java에서 스미스 번호를 확인하는 방법에 대해 논의합니다. 코드 구현의 예.

이 기사에서는 가장 많이 묻는 Java Spring 면접 질문과 자세한 답변을 보관했습니다. 그래야 면접에 합격할 수 있습니다.

Java 8은 스트림 API를 소개하여 데이터 컬렉션을 처리하는 강력하고 표현적인 방법을 제공합니다. 그러나 스트림을 사용할 때 일반적인 질문은 다음과 같은 것입니다. 기존 루프는 조기 중단 또는 반환을 허용하지만 스트림의 Foreach 메소드는이 방법을 직접 지원하지 않습니다. 이 기사는 이유를 설명하고 스트림 처리 시스템에서 조기 종료를 구현하기위한 대체 방법을 탐색합니다. 추가 읽기 : Java Stream API 개선 스트림 foreach를 이해하십시오 Foreach 메소드는 스트림의 각 요소에서 하나의 작업을 수행하는 터미널 작동입니다. 디자인 의도입니다

Java의 TimeStamp to Date 안내. 여기서는 소개와 예제와 함께 Java에서 타임스탬프를 날짜로 변환하는 방법에 대해서도 설명합니다.

캡슐은 3 차원 기하학적 그림이며, 양쪽 끝에 실린더와 반구로 구성됩니다. 캡슐의 부피는 실린더의 부피와 양쪽 끝에 반구의 부피를 첨가하여 계산할 수 있습니다. 이 튜토리얼은 다른 방법을 사용하여 Java에서 주어진 캡슐의 부피를 계산하는 방법에 대해 논의합니다. 캡슐 볼륨 공식 캡슐 볼륨에 대한 공식은 다음과 같습니다. 캡슐 부피 = 원통형 볼륨 2 반구 볼륨 안에, R : 반구의 반경. H : 실린더의 높이 (반구 제외). 예 1 입력하다 반경 = 5 단위 높이 = 10 단위 산출 볼륨 = 1570.8 입방 단위 설명하다 공식을 사용하여 볼륨 계산 : 부피 = π × r2 × h (4

PHP와 Python은 각각 고유 한 장점이 있으며 선택은 프로젝트 요구 사항을 기반으로해야합니다. 1.PHP는 간단한 구문과 높은 실행 효율로 웹 개발에 적합합니다. 2. Python은 간결한 구문 및 풍부한 라이브러리를 갖춘 데이터 과학 및 기계 학습에 적합합니다.

PHP는 서버 측에서 널리 사용되는 스크립팅 언어이며 특히 웹 개발에 적합합니다. 1.PHP는 HTML을 포함하고 HTTP 요청 및 응답을 처리 할 수 있으며 다양한 데이터베이스를 지원할 수 있습니다. 2.PHP는 강력한 커뮤니티 지원 및 오픈 소스 리소스를 통해 동적 웹 컨텐츠, 프로세스 양식 데이터, 액세스 데이터베이스 등을 생성하는 데 사용됩니다. 3. PHP는 해석 된 언어이며, 실행 프로세스에는 어휘 분석, 문법 분석, 편집 및 실행이 포함됩니다. 4. PHP는 사용자 등록 시스템과 같은 고급 응용 프로그램을 위해 MySQL과 결합 할 수 있습니다. 5. PHP를 디버깅 할 때 error_reporting () 및 var_dump ()와 같은 함수를 사용할 수 있습니다. 6. 캐싱 메커니즘을 사용하여 PHP 코드를 최적화하고 데이터베이스 쿼리를 최적화하며 내장 기능을 사용하십시오. 7

Java는 초보자와 숙련된 개발자 모두가 배울 수 있는 인기 있는 프로그래밍 언어입니다. 이 튜토리얼은 기본 개념부터 시작하여 고급 주제를 통해 진행됩니다. Java Development Kit를 설치한 후 간단한 "Hello, World!" 프로그램을 작성하여 프로그래밍을 연습할 수 있습니다. 코드를 이해한 후 명령 프롬프트를 사용하여 프로그램을 컴파일하고 실행하면 "Hello, World!"가 콘솔에 출력됩니다. Java를 배우면 프로그래밍 여정이 시작되고, 숙달이 깊어짐에 따라 더 복잡한 애플리케이션을 만들 수 있습니다.
