ホームページ  >  記事  >  Java  >  Javaの文字列オブジェクトを深く理解する

Javaの文字列オブジェクトを深く理解する

angryTom
angryTom転載
2019-11-27 14:42:282477ブラウズ

Javaの文字列オブジェクトを深く理解する

ここでは、Java の String オブジェクトについて少し詳しく説明します。

Java オブジェクト実装の進化

String オブジェクトは Java で最も頻繁に使用されるオブジェクトの 1 つであるため、Java 開発者も常に String オブジェクトを実装しています。 String オブジェクトのパフォーマンス。

(推奨学習: Java ビデオ チュートリアル )

Java6 および以前のバージョンの String オブジェクトの属性

Java6 以前のバージョンでは、String オブジェクトは char 配列をカプセル化するオブジェクトで、主に 4 つのメンバー変数 (char 配列、オフセット offset、文字数、ha Hope 値ハッシュ) を持ちます。 String オブジェクトは char[] 配列を見つけ、offset と count の 2 つの属性を通じて文字列を取得します。これにより、メモリ領域を節約しながら配列オブジェクトを効率的かつ迅速に共有できますが、この方法ではメモリ リークが発生する可能性があります。

Java7 および 8 バージョンの String オブジェクトの属性

Java7 バージョンから、Java は String クラスにいくつかの変更を加えました。 2 つの変数をオフセットおよびカウントします。この利点は、String オブジェクトが占有するメモリがわずかに少なくなり、String.substring() メソッドが char[] を共有しなくなるため、このメソッドの使用によって発生する可能性のあるメモリ リークの問題が解決されることです。

Java9 以降のバージョンの String オブジェクトの属性

Java9 バージョン以降、Java は char[] 配列を byte[] 配列に変更しました。 char が 2 バイトであることは誰もが知っていますが、1 バイトを格納するために使用すると、メモリ領域の無駄が発生します。この 1 バイトのスペースを節約するために、Java 開発者は文字列の格納に 1 バイトを使用するように変更しました。

さらに、Java9 では、String オブジェクトは新しい属性コーダーを維持します。この属性は、エンコード形式の識別子です。文字列の長さを計算するとき、または、indexOf() メソッドを呼び出すときに、これを使用する必要があります。文字列の長さを計算する方法を決定するフィールド。 coder 属性にはデフォルトで 0 と 1 の 2 つの値があり、0 は Latin-1 (シングルバイト エンコーディング) を表し、1 は UTF-16 エンコーディングを表します。

String オブジェクトの作成とメモリへの格納方法

Java では、基本データ型の変数とオブジェクトへの参照はスタック メモリに格納されます。 ; new キーワードとコンストラクタによって作成されたオブジェクトはヒープ メモリに格納されます。一般に、String オブジェクトを作成するには 2 つの方法があり、1 つはリテラル (文字列定数)、もう 1 つはコンストラクター (String()) であり、2 つのメソッドはメモリ内に異なる方法で保存されます。

リテラル (文字列定数) の作成方法

リテラルを使用して文字列を作成する場合、JVM はまず文字列定数プールにリテラルが存在するかどうかを確認します。リテラルが存在する場合は、の場合、メモリ内のリテラルの参照アドレスが返されます。存在しない場合は、文字列定数プール内にリテラルが作成され、参照が返されます。この方法で作成する利点は、同じ値の文字列がメモリ上に繰り返し作成されることを避け、メモリを節約できることと、この書き方の方がシンプルで読みやすいことです。

String str = "i like yanggb.";

文字列定数プール

ここでは、定数プールについて特別に説明します。定数プールは、文字列オブジェクトの繰り返し作成を減らすために JVM によって維持される特別なメモリであり、文字列定数プールまたは文字列リテラル プールと呼ばれます。 JDK1.6以前のバージョンでは、ランタイム定数プールはメソッド領域にあります。 JDK1.7 以降のバージョンの JVM では、実行時定数プールがメソッド領域の外に移動され、実行時定数プールを格納するための領域が Java ヒープ (ヒープ) 内に開かれています。 JDK1.8 以降、JVM は Java メソッド領域をキャンセルし、ダイレクト メモリに配置されたメタスペース (MetaSpace) に置き換えました。要約すると、現在の文字列定数プールはヒープ内にあります。

私たちが知っているいくつかの String オブジェクトの特性はすべて String 定数プールから来ています。

1. すべての String オブジェクトは定数プールで共有されるため、一度変更されると、この String オブジェクトを参照するすべての変数もそれに応じて変更されるため、String オブジェクトを変更することはできません (参照が変更されます)。 String オブジェクトは不変になるように設計されており、この不変機能については後ほど詳しく説明します。

2. String オブジェクトは文字列を結合するときにパフォーマンスが低いということも、これに由来しています。String オブジェクトの不変の性質のため、各変更 (ここでは結合) は新しい文字列オブジェクトを返します。元の文字列オブジェクトを変更し、新しい文字列オブジェクトを作成すると、より多くのパフォーマンスが消費されます (追加のメモリ領域が解放されます)。

3.因为常量池中创建的String对象是共享的,因此使用双引号声明的String对象(字面量)会直接存储在常量池中,如果该字面量在之前已存在,则是会直接引用已存在的String对象,这一点在上面已经描述过了,这里再次提及,是为了特别说明这一做法保证了在常量池中的每个String对象都是唯一的,也就达到了节约内存的目的。

构造函数(String())的创建方式

使用构造函数的方式创建字符串时,JVM同样会在字符串常量池中先检查是否存在该字面量,只是检查后的情况会和使用字面量创建的方式有所不同。如果存在,则会在堆中另外创建一个String对象,然后在这个String对象的内部引用该字面量,最后返回该String对象在内存地址中的引用;如果不存在,则会先在字符串常量池中创建该字面量,然后再在堆中创建一个String对象,然后再在这个String对象的内部引用该字面量,最后返回该String对象的引用。

String str = new String("i like yanggb.");

这就意味着,只要使用这种方式,构造函数都会另行在堆内存中开辟空间,创建一个新的String对象。具体的理解是,在字符串常量池中不存在对应的字面量的情况下,new String()会创建两个对象,一个放入常量池中(字面量),一个放入堆内存中(字符串对象)。

String对象的比较

比较两个String对象是否相等,通常是有【==】和【equals()】两个方法。

在基本数据类型中,只可以使用【==】,也就是比较他们的值是否相同;而对于对象(包括String)来说,【==】比较的是地址是否相同,【equals()】才是比较他们内容是否相同;而equals()是Object都拥有的一个函数,本身就要求对内部值进行比较。

String str = "i like yanggb.";
String str1 = new String("i like yanggb.");
System.out.println(str == str1); // falseSystem.out.println(str.equals(str1)); // true

因为使用字面量方式创建的String对象和使用构造函数方式创建的String对象的内存地址是不同的,但是其中的内容却是相同的,也就导致了上面的结果。

String对象中的intern()方法

我们都知道,String对象中有很多实用的方法。为什么其他的方法都不说,这里要特别说明这个intern()方法呢,因为其中的这个intern()方法最为特殊。它的特殊性在于,这个方法在业务场景中几乎用不上,它的存在就是在为难程序员的,也可以说是为了帮助程序员了解JVM的内存结构而存在的(?我信你个鬼,你个糟老头子坏得很)。

/*** 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.
**/public native String intern();

上面是源码中的intern()方法的官方注释说明,大概意思就是intern()方法用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后返回引用。然后我们可以从方法签名上看出intern()方法是一个native方法。

下面通过几个例子来详细了解下intern()方法的用法。

第一个例子

String str1 = new String("1");
System.out.println(str1 == str1.intern()); // falseSystem.out.println(str1 == "1"); // false

在上面的例子中,intern()方法返回的是常量池中的引用,而str1保存的是堆中对象的引用,因此两个打印语句的结果都是false。

第二个例子

String str2 = new String("2") + new String("3");
System.out.println(str2 == str2.intern()); // trueSystem.out.println(str2 == "23"); // true

在上面的例子中,str2保存的是堆中一个String对象的引用,这和JVM对【+】的优化有关。实际上,在给str2赋值的第一条语句中,创建了3个对象,分别是在字符串常量池中创建的2和3、还有在堆中创建的字符串对象23。因为字符串常量池中不存在字符串对象23,所以这里要特别注意:intern()方法在将堆中存在的字符串对象加入常量池的时候采取了一种截然不同的处理方案——不是在常量池中建立字面量,而是直接将该String对象自身的引用复制到常量池中,即常量池中保存的是堆中已存在的字符串对象的引用。根据前面的说法,这时候调用intern()方法,就会在字符串常量池中复制出一个对堆中已存在的字符串常量的引用,然后返回对字符串常量池中这个对堆中已存在的字符串常量池的引用的引用(就是那么绕,你来咬我呀)。这样,在调用intern()方法结束之后,返回结果的就是对堆中该String对象的引用,这时候使用【==】去比较,返回的结果就是true了。同样的,常量池中的字面量23也不是真正意义的字面量23了,它真正的身份是堆中的那个String对象23。这样的话,使用【==】去比较字面量23和str2,结果也就是true了。

第三个例子

String str4 = "45";
String str3 = new String("4") + new String("5");
System.out.println(str3 == str3.intern()); // falseSystem.out.println(str3 == "45"); // false

这个例子乍然看起来好像比前面的例子还要复杂,实际上却和上面的第一个例子是一样的,最难理解的反而是第二个例子。

所以这里就不多说了,而至于为什么还要举这个例子,我相信聪明的你一下子就明白了。

String对象的不可变性

先来看String对象的一段源码。

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
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
}

从类签名上来看,String类用了final修饰符,这就意味着这个类是不能被继承的,这是决定String对象不可变特性的第一点。从类中的数组char[] value来看,这个类成员变量被private和final修饰符修饰,这就意味着其数值一旦被初始化之后就不能再被更改了,这是决定String对象不可变特性的第二点。

Java开发者为什么要将String对象设置为不可变的,主要可以从以下三个方面去考虑:

1.安全性。假设String对象是可变的,那么String对象将可能被恶意修改。

2.唯一性。这个做法可以保证hash属性值不会频繁变更,也就确保了唯一性,使得类似HashMap的容器才能实现相应的key-value缓存功能。

3.功能性。可以实现字符串常量池(究竟是先有设计,还是先有实现呢)。

String对象的优化

字符串是常用的Java类型之一,所以对字符串的操作是避免不了的。而在对字符串的操作过程中,如果使用不当的话,性能可能会有天差地别,所以有一些地方是要注意一下的。

拼接字符串的性能优化

字符串的拼接是对字符串的操作中最频繁的一个使用。由于我们都知道了String对象的不可变性,所以我们在开发过程中要尽量减少使用【+】进行字符串拼接操作。这是因为使用【+】进行字符串拼接,会在得到最终想要的结果前产生很多无用的对象。

String str = 'i';
str = str + ' ';
str = str + 'like';
str = str + ' ';
str = str + 'yanggb';
str = str + '.';
System.out.println(str); // i like yanggb.

事实上,如果我们使用的是比较智能的IDE编写代码的话,编译器是会提示将代码优化成使用StringBuilder或者StringBuffer对象来优化字符串的拼接性能的,因为StringBuilder和StringBuffer都是可变对象,也就避免了过程中产生无用的对象了。而这两种替代方案的区别是,在需要线程安全的情况下,选用StringBuffer对象,这个对象是支持线程安全的;而在不需要线程安全的情况下,选用StringBuilder对象,因为StringBuilder对象的性能在这种场景下,要比StringBuffer对象或String对象要好得多。

使用intern()方法优化内存占用

前面吐槽了intern()方法在实际开发中没什么用,这里又来说使用intern()方法来优化内存占用了,这人真的是,嘿嘿,真香。关于方法的使用就不说了,上面有详尽的用法说明,这里来说说具体的应用场景好了。有一位Twitter的工程师在Qcon全球软件开发大会上分享了一个他们对String对象优化的案例,他们利用了这个String.intern()方法将以前需要20G内存存储优化到只需要几百兆内存。具体就是,使用intern()方法将原本需要创建到堆内存中的String对象都放到常量池中,因为常量池的不重复特性(存在则返回引用),也就避免了大量的重复String对象造成的内存浪费问题。

什么,要我给intern()方法道歉?不可能。String.intern()方法虽好,但是也是需要结合场景来使用的,并不能够乱用。因为实际上,常量池的实现是类似于一个HashTable的实现方式,而HashTable存储的数据越大,遍历的时间复杂度就会增加。这就意味着,如果数据过大的话,整个字符串常量池的负担就会大大增加,有可能性能不会得到提升却反而有所下降。

字符串分割的性能优化

字符串的分割是字符串操作的常用操作之一,对于字符串的分割,大部分人使用的都是split()方法,split()方法在大部分场景下接收的参数都是正则表达式,这种分割方式本身没有什么问题,但是由于正则表达式的性能是非常不稳定的,使用不恰当的话可能会引起回溯问题并导致CPU的占用居高不下。在以下两种情况下split()方法不会使用正则表达式:

1.传入的参数长度为1,且不包含“.$|()[{^?*+\”regex元字符的情况下,不会使用正则表达式。

2.传入的参数长度为2,第一个字符是反斜杠,并且第二个字符不是ASCII数字或ASCII字母的情况下,不会使用正则表达式。

所以我们在字符串分割时,应该慎重使用split()方法,而首先考虑使用String.indexOf()方法来进行字符串分割,在String.indexOf()无法满足分割要求的时候再使用Split()方法。而在使用split()方法分割字符串时,需要格外注意回溯问题。

总结

String オブジェクトを理解していなくても開発に String オブジェクトを使用できますが、String オブジェクトを理解すると、より良いコードを作成するのに役立ちます。

「物語が終わっても、私は私であり、あなたはあなたであり続けることを願っています。」

推奨チュートリアル: java チュートリアル

以上がJavaの文字列オブジェクトを深く理解するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcnblogs.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。