关于Java String的一些总结
作者:大飞
- 不变模式设计
String是典型的不变模式设计。主要体现在两点:
1.String Class由final修饰,保证了无法产生可改变String语义的子类。
2.一些有"修改"行为的方法都会产生新的String实例,如concat、replace等方法。
- ==相关的问题
看一些常见的判断String相等的问题,给一段代码:
public class StringEqualsTest { public static final String s1 = "5"; public static String s2 = "5"; public static String getS(){ return "5"; } public static void main(String[] args) { String a1 = "12"; String a2 = "12"; String a3 = new String("12"); String a4 = new String("12"); System.out.println(a1 == a2); //1 System.out.println(a3 == a4); //2 System.out.println(a1 == a3); //3 System.out.println("============"); //============================== String b1 = "34"; String b2 = "3" + "4"; String b3 = 3 + "4"; String b4 = "3" + 4; System.out.println(b1 == b2); //4 System.out.println(b1 == b3); //5 System.out.println(b1 == b4); //6 System.out.println("============"); //============================== String c1 = "56"; String c2 = "5"; String c3 = "6"; String c4 = c2 + "6"; String c5 = c2 + c3; final String c6 = "5"; String c7 = c6 + "6"; String c8 = s1 + "6"; String c9 = s2 + "6"; String c0 = getS() + "6"; System.out.println(c1 == c4); //7 System.out.println(c1 == c5); //8 System.out.println(c1 == c7); //9 System.out.println(c1 == c8); //10 System.out.println(c1 == c9); //11 System.out.println(c1 == c0); //12 } }输出如下:(运行环境JDK1.8)
true false false ============ true true true ============ false false true true false false
分析:
情况1:a1和a2都来自字符串池,所以为true。
情况2:a3和a4分别是堆内存里不同的对象,所以为false。
情况3:a1来自字符串池,a3是堆内存新建的对象,所以为false。
情况4:b1来自字符串池,b2由于编译器优化,变成"34",从字符串池中获取,所以为true。
情况5:b1来自字符串池,b3由于编译器优化,变成"34",从字符串池中获取,所以为true。
情况6:b1来自字符串池,b4由于编译器优化,变成"34",从字符串池中获取,所以为true。
情况7:c1来自字符串池,c4编译后变成StringBuilder的拼接(append),最后由StringBuilder的toString方法返回一个新的String实例,所以为false。
情况8:c1来自字符串池,c5同c4,所以为false。
情况9:c1来自字符串池,c7由于编译器优化(c6由final修饰,不会变了,可以优化),变成"56",所以为true。
情况10:c1来自字符串池,c8由于编译器优化(s1为常量,不会变了,可以优化),变成"56",所以为true。
情况11:c1来自字符串池,c9同c4(s2为静态变量,但编译器无法保证s2是否会发生变化,所以无法直接优化成"56"),所以为false。
情况12:c1来自字符串池,c0同c4(编译器无法保证getS方法返回"56"且不变,所以无法直接优化成"56"),所以为false。
- substring的一些历史
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); } String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; }
可见,substring后,返回的新字符串会和旧字符串共享内部的value(char数组),这样看似合理共享利用的内存,但可能会带来一些问题:如果程序从外部读到了一个大字符串,然后只需要大字符串的一部分,做了substring,但实际上还是会hold住整个大字符串的内容在内存里面。如果这样的情况很多,就可能会出现问题。
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 ... public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException(endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); } public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count < 0) { throw new StringIndexOutOfBoundsException(count); } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); }
可见,jdk1.7后,substring内部返回的新字符串不会共享旧字符串的内部字符数组,而是会按需拷贝一个新的数组。
- 字符串池相关
public native String intern();
这个方法的意思是:如果当前字符串存在于字符串池中,就返回池中的字符串对象的引用;否则添加当前字符串对象到字符串池中,然后返回当前字符串对象的引用。
举个栗子:public static void main(String[] args) { String s = new String(new char[]{'1','4','7'}); s.intern(); System.out.println(s == "147"); String s2 = new String("258"); s2.intern(); System.out.println(s2 == "258"); }输出如下:
true false
你猜对了吗?修正:jdk版本为1.8
来看下intern内部怎么实现的。
打开openjdk代码,找到openjdk\jdk\src\share\native\java\lang目录,找到String.c代码如下:#include "jvm.h" #include "java_lang_String.h" JNIEXPORT jobject JNICALL Java_java_lang_String_intern(JNIEnv *env, jobject this) { return JVM_InternString(env, this); }然后找到实现,在hotspot\src\share\vm\prims\jvm.cpp中:
// String support /////////////////////////////////////////////////////////////////////////// JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str)) JVMWrapper("JVM_InternString"); JvmtiVMObjectAllocEventCollector oam; if (str == NULL) return NULL; oop string = JNIHandles::resolve_non_null(str); oop result = StringTable::intern(string, CHECK_NULL); return (jstring) JNIHandles::make_local(env, result); JVM_END可见,这里是调用了StringTable的intern方法,看下StringTable的定义和相关实现,定义在hotspot\src\share\vm\classfile\symbolTable.hpp中:
class StringTable : public RehashableHashtable<oop, mtSymbol> { friend class VMStructs; private: // The string table static StringTable* _the_table; ... public: // The string table static StringTable* the_table() { return _the_table; } // Size of one bucket in the string table. Used when checking for rollover. static uint bucket_size() { return sizeof(HashtableBucket<mtSymbol>); } static void create_table() { assert(_the_table == NULL, "One string table allowed."); _the_table = new StringTable(); }StringTable就是一个hash表。
再看下相关的实现,实现在hotspot\src\share\vm\classfile\symbolTable.cpp中:
oop StringTable::intern(oop string, TRAPS) { if (string == NULL) return NULL; ResourceMark rm(THREAD); int length; Handle h_string (THREAD, string); jchar* chars = java_lang_String::as_unicode_string(string, length, CHECK_NULL); oop result = intern(h_string, chars, length, CHECK_NULL); return result; } oop StringTable::intern(Handle string_or_null, jchar* name, int len, TRAPS) { unsigned int hashValue = hash_string(name, len); int index = the_table()->hash_to_index(hashValue); oop found_string = the_table()->lookup(index, name, len, hashValue); // Found if (found_string != NULL) { ensure_string_alive(found_string); return found_string; } debug_only(StableMemoryChecker smc(name, len * sizeof(name[0]))); assert(!Universe::heap()->is_in_reserved(name), "proposed name of symbol must be stable"); Handle string; // try to reuse the string if possible if (!string_or_null.is_null()) { string = string_or_null; } else { string = java_lang_String::create_from_unicode(name, len, CHECK_NULL); } #if INCLUDE_ALL_GCS if (G1StringDedup::is_enabled()) { // Deduplicate the string before it is interned. Note that we should never // deduplicate a string after it has been interned. Doing so will counteract // compiler optimizations done on e.g. interned string literals. G1StringDedup::deduplicate(string()); } #endif // Grab the StringTable_lock before getting the_table() because it could // change at safepoint. oop added_or_found; { MutexLocker ml(StringTable_lock, THREAD); // Otherwise, add to symbol to table added_or_found = the_table()->basic_add(index, string, name, len, hashValue, CHECK_NULL); } ensure_string_alive(added_or_found); return added_or_found; }
逻辑很简单,首先从StringTable中找给定的字符串对象,找到的话就直接返回,找不到就创建一个字符串对象,然后添加到StringTable中,然后返回其引用。
jdk1.6与jdk1.7、1.8的字符串池实现的一点区别:
1.6的字符串池实现在PermGen区中而1.7和1.8的字符串池实现在堆内存里面。相对于1.6来说,在1.7或1.8下使用字符串池,受限制的堆内存的大小,而不是相对较小的PermGen的大小。
1.7和1.8也提供了相关JVM参数来调整和观察StringTable,参数有:
-XX:StringTableSize 设置StringTable的size。
-XX:+PrintStringTableStatistics 在程序结束时打印StringTable的一些使用情况。
public static void main(String[] args) { int n = 0; for(int i=0;i<10000000;i++){ String s = ""+n++; s.intern(); } System.out.println("over"); }
运行时jvm参数为:-XX:+PrintStringTableStatistics
输出如下:over SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 27283 = 654792 bytes, avg 24.000 Number of literals : 27283 = 1166312 bytes, avg 42.749 Total footprint : = 1981192 bytes Average bucket size : 1.363 Variance of bucket size : 1.344 Std. dev. of bucket size: 1.159 Maximum bucket size : 8 StringTable statistics: Number of buckets : 60013 = 480104 bytes, avg 8.000 Number of entries : 815780 = 19578720 bytes, avg 24.000 Number of literals : 815780 = 45819984 bytes, avg 56.167 Total footprint : = 65878808 bytes Average bucket size : 13.593 Variance of bucket size : 7.484 Std. dev. of bucket size: 2.736 Maximum bucket size : 23
我们可以看到StringTable的使用情况。同时也能看到默认的StringTableSize是60013(运行的jdk版本是jdk1.8.0_25)。
over SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 27273 = 654552 bytes, avg 24.000 Number of literals : 27273 = 1165864 bytes, avg 42.748 Total footprint : = 1980504 bytes Average bucket size : 1.363 Variance of bucket size : 1.343 Std. dev. of bucket size: 1.159 Maximum bucket size : 8 StringTable statistics: Number of buckets : 120013 = 960104 bytes, avg 8.000 Number of entries : 796602 = 19118448 bytes, avg 24.000 Number of literals : 796602 = 44744552 bytes, avg 56.169 Total footprint : = 64823104 bytes Average bucket size : 6.638 Variance of bucket size : 4.038 Std. dev. of bucket size: 2.010 Maximum bucket size : 15
2016年1月8号补充=================================================
今天看到一个讨论,下面的代码在jdk1.6和jdk1.7之后版本,结果不同:
(这里的代码并非讨论中的代码,但说的是同一件事。为了和上面的例子承接)
public static void main(String[] args) { String s = new String(new char[]{'1','4','7'}); s.intern(); String s2 = "147"; System.out.println(s == s2); String s3 = "258"; s3.intern(); String s4 = "258"; System.out.println(s3 == s4); }
经过测试上面的程序在jdk1.6下输出false true;在jdk1.8下输出true、true。
所以我前面那个“让你猜”的例子是不够严谨的,当时没有重点说明运行版本。
下面看下为什么会这样:
在1.6版本的openjdk代码里,StringTable的oop StringTable::intern(Handle string_or_null, jchar* name,int len, TRAPS)方法实现中有这么一行:
// try to reuse the string if possible if (!string_or_null.is_null() && (!JavaObjectsInPerm || string_or_null()->is_perm())) { string = string_or_null; } else { string = java_lang_String::create_tenured_from_unicode(name, len, CHECK_NULL); }而在1.8版本的openjdk代码里,这行是这样的:
// try to reuse the string if possible if (!string_or_null.is_null()) { string = string_or_null; } else { string = java_lang_String::create_from_unicode(name, len, CHECK_NULL); }
我们重点关注里面的条件语句,1.6下会多出这个条件
(!JavaObjectsInPerm || string_or_null()->is_perm())
这个JavaObjectsInPerm没太深究,但猜测这里显然是true,如果是false的话这句条件就没意义了。接下来会判断一下string_or_null是不是perm中的对象,这句比较关键:
如果string_or_null是perm中的对象,那么if条件成立,最终会将这个string_or_null放入StringTable中;反之,if条件不成立,最终会再次创建一个String对象,然后放入StringTable中。
so,再回头看看刚给出的代码,在1.6版本下,开始创建一个String s = new String(new char[]{'1','4','7'})的对象,然后调用intern方法,由于并未出现"147"这种字面量,所以接下来的intern方法中,会在StringTable中找不到对象,然后走上面提到的逻辑,那么接下来会判断s是否在perm里,显然不是,所以intern中新建了一个String对象,放到StringTable中,下一句String s2 = "147" 是从StringTable中拿的对象,所以s和s2当然不是同一个对象了。
如果是1.8版本,只要走了上面的逻辑,就会将String对象加入StringTable,没有是否存在于perm的判断了。所以s和s2还是同一个对象。
OK!关于Java String的一些总结就到这里。
相关源码:
相关推荐
根据提供的信息,我们可以总结出这份Java基础String类选择题练习题主要聚焦于String及StringBuffer类的使用。尽管具体的题目内容未给出,但从所展示的信息中可以推断出该练习题集涵盖了以下几方面的知识点: ### 一...
### JAVA String 类函数总结 #### 一、字符串创建与初始化 在 Java 中,`String` 类是最常用的字符串处理工具之一。它可以用来表示一系列字符,并且提供了丰富的方法来操作这些字符。下面通过示例代码展示了如何...
本文档将对`String`类型的一些关键知识点进行总结,包括字符串的格式化、特殊运算操作、进制转换以及与之相关的其他实用技巧。 #### 二、字符串格式化 1. **小数保留指定位数** - 示例代码:`double sum = r * r *...
这份"javaString总结共13页.pdf.zip"压缩包文件显然包含了关于Java字符串的深入讲解,覆盖了多个关键知识点。虽然没有提供具体的PDF内容,但我可以基于常见的Java String主题为你概述一些重要的概念。 1. **字符串...
本篇文章将深入探讨`String`类的一些常用方法,并通过实际例子帮助你更好地理解和运用这些方法。 ### 1. 字符串排序 在**字符串练习一**中,我们展示了如何使用`compareTo`方法对字符串数组进行排序。`compareTo`...
### JAVA中关于String的一些注意点 在Java编程语言中,`String` 类是最常用的数据类型之一,用于处理文本数据。本文将深入探讨Java中的`String`类及其使用时需要注意的关键点,希望对开发者们有所帮助。 #### 1. ...
### Java中String、十六进制String与byte[]之间的相互转换 在Java开发中,字符串(String)、十六进制表示的字符串以及字节数组(byte[])之间的转换是非常常见的需求,尤其是在处理网络通信、文件读写等场景下。...
### JAVA中String与StringBuffer的区别 在Java编程语言中,`String` 和 `StringBuffer` 都是非常重要的类,它们用于处理字符串数据。虽然它们都实现了字符串操作的功能,但它们之间存在一些重要的区别,这些区别...
### Java字符串操作详解:String1.java程序分析 在Java编程语言中,字符串处理是一项非常重要的技能,无论是进行数据处理还是用户交互,字符串都是一个不可或缺的数据类型。本篇将基于提供的`String1.java`代码示例...
总结来说,理解Java中`null`、空字符串`""`的区别,以及如何安全地进行比较,是编写健壮代码的关键。同时,了解字符串对象的内存地址和内容相等性的差异,可以帮助我们更好地理解和调试程序。在实际开发中,应始终...
在这个文档中,我们主要讨论了关于Java String类的一些常用方法以及与String相关的常量池问题。 首先,我们要理解Java中的final关键字。当final修饰一个引用变量时,它确保这个引用变量一旦被初始化,就不能再指向...
### JAVA中String与byte[]的关系解析 在Java编程语言中,`String`对象与`byte[]`数组之间的转换是常见的操作之一。理解这两者之间的关系对于处理文本数据、网络通信及文件读写等任务至关重要。 #### 一、String与...
深入了解Java中的String类是至关重要的,因为String在Java编程中占据着极其重要的位置。下面将对给定的信息进行深入分析: ### 1. String 类是 final 的,不可被继承 在Java中,`String` 类被声明为 `final` 类型...
总结,JavaBean和JsonString的转换是Java开发中的常见操作,通过各种JSON库可以轻松实现。了解这些转换方法对于进行前后端数据交互、序列化和反序列化等任务至关重要。在实际开发中,应根据项目需求选择合适的库,并...
Java-String类的常用方法总结,String类在java.lang包中,java使用String类创建一个字符串变量,字符串变量属于对象。java把String类声明的final类,不能有类。String类对象创建后不能修改,由0或多个字符组成,包含...
总结来说,Java中的String和StringBuilder各有优劣: - String:不可变,线程安全,适用于不需要频繁修改字符串的场景。 - StringBuilder:可变,性能更高,适用于大量字符串操作的场合,单线程环境首选。 - ...
在这个总结中,我们将深入探讨String类的一些核心特性和方法。 首先,String类位于`java.lang`包中,这意味着它对所有Java程序都是自动导入的,无需额外引用。String类被声明为final,这意味着我们无法创建其子类,...
简单总结可以下Java中String类中的常用方法