程序地带

深入理解java中的String类


在Java语言中,所有类似“ABC”的字面值,都是String类的实例;String类位于java.lang包下,是Java语言的核心类,提供了字符串的比较、查找、截取、大小写转换等操作;Java语言为“+”连接符(字符串连接符)以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。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
...
}

从上面可以看出 1)String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。 2)String类实现了Serializable、CharSequence、 Comparable接口。 3)String实例的值是通过字符数组实现字符串存储的。


1.1 “+”连接符的实现原理 Java语言为“+”连接符以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。其中字符串连接是通过 StringBuilder(或 StringBuffer)类及其append 方法实现的,对象转换为字符串是通过 toString 方法实现的,该方法由 Object 类定义,并可被 Java 中的所有类继承。有关字符连接和转换的更多信息,可以参阅 Gosling、Joy 和 Steele 合著的 《The Java Language Specification》


我们可以通过反编译验证一下


/**
* 测试代码
*/
public class Test {
public static void main(String[] args) {
int i = 10;
String s = "abc";
System.out.println(s + i);
}
}
/**
* 反编译后
*/
public class Test {
public static void main(String args[]) { //删除了默认构造函数和字节码
byte byte0 = 10;
String s = "abc";
System.out.println((new StringBuilder()).append(s).append(byte0).toString());
}
}

由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。由于append()方法的各种重载形式会调用String.valueOf方法,所以我们可以认为:


//以下两者是等价的
s = i + ""
s = String.valueOf(i);
//以下两者也是等价的
s = "abc" + i;
s = new StringBuilder("abc").append(i).toString();

1.2 “+”连接符的效率 使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。


String s = "abc";
for (int i=0; i<10000; i++) {
s += "abc";
}
/**
* 反编译后
*/
String s = "abc";
for(int i = 0; i < 1000; i++) {
s = (new StringBuilder()).append(s).append("abc").toString();
}

这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。


/**
* 循环中使用StringBuilder代替“+”连接符
*/
StringBuilder sb = new StringBuilder("abc");
for (int i = 0; i < 1000; i++) {
sb.append("abc");
}
sb.toString();

与此之外还有一种特殊情况,也就是当"+"两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如:


System.out.println("Hello" + "World");
/**
* 反编译后
*/
System.out.println("HelloWorld");
/**
* 编译期确定
* 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
* 所以此时的"a" + s1和"a" + "b"效果是一样的。故结果为true。
*/
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = true
/**
* 编译期无法确定
* 这里面虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定
* 因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
*/
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = false
public String getS1() {
return "b";
}

综上,“+”连接符对于直接相加的字符串常量效率很高,因为在编译期间便确定了它的值,也就是说形如"I"+“love”+“java”; 的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用,且编译期无法确定值的),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。 2. 字符串常量池


在Java的内存分配中,总共3种常量池,分别是Class常量池、运行时常量池、字符串常量池。


字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。


/**
* 字符串常量池中的字符串只存在一份!
* 运行结果为true
*/
String s1 = "hello world!";
String s2 = "hello world!";
System.out.println(s1 == s2);

2.1 内存区域


在HotSpot VM中字符串常量池是通过一个StringTable类实现的,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例中只有一份,被所有的类共享;字符串常量由一个一个字符组成,放在了StringTable上。要注意的是,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。


在JDK6及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中的,StringTable的长度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,StringTable的长度可以通过**-XX:StringTableSize=66666*参数指定。至于JDK7为什么把常量池移动到堆上实现,原因可能是由于方法区的内存空间太小且不方便扩展,而堆的内存空间比较大且扩展方便。


2.2 存放的内容 在JDK6及之前版本中,String Pool里放的都是字符串常量;在JDK7.0中,由于String.intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。


/**
* 运行结果为true false
*/
String s1 = "AB";
String s2 = "AB";
String s3 = new String("AB");
System.out.println(s1 == s2);
System.out.println(s1 == s3);

由于常量池中不存在两个相同的对象,所以s1和s2都是指向JVM字符串常量池中的"AB"对象。new关键字一定会产生一个对象,并且这个对象存储在堆中。所以String s3 = new String(“AB”);产生了两个对象:保存在栈中的s3和保存堆中的String对象。 在这里插入图片描述 当执行String s1 = "AB"时,JVM首先会去字符串常量池中检查是否存在"AB"对象,如果不存在,则在字符串常量池中创建"AB"对象,并将"AB"对象的地址返回给s1;如果存在,则不创建任何对象,直接将字符串常量池中"AB"对象的地址返回给s1。


3. intern方法 直接使用双引号声明出来的String对象会直接存储在字符串常量池中,如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。


JDK1.7的改动:


将String常量池 从 Perm 区移动到了 Java Heap区 String.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();

3.1 intern的用法


static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];
public static void main(String[] args) throws Exception {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = random.nextInt();
}
long t = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
//arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
}
System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}

运行的参数是:-Xmx2g -Xms2g -Xmn1500M 上述代码是一个演示代码,其中有两条语句不一样,一条是未使用 intern,一条是使用 intern。结果如下图


未使用intern,耗时826ms: 在这里插入图片描述 使用intern,耗时2160ms: 在这里插入图片描述 通过上述结果,我们发现不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间。 使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。


细心的同学会发现使用了 intern 方法后时间上有了一些增长。这是因为程序中每次都是用了 new String 后,然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern 占用空间导致 jvm 垃圾回收的时间是要远远大于这点时间的。 毕竟这里使用了1000w次intern 才多出来1秒钟多的时间。


4. String、StringBuilder和StringBuffer 4.1 继承结构 在这里插入图片描述 4.2 主要区别 1)String是不可变字符序列,StringBuilder和StringBuffer是可变字符序列。 2)执行速度StringBuilder > StringBuffer > String。 3)StringBuilder是非线程安全的,StringBuffer是线程安全的。


5. 总结


常见的题目:


public static void main(String[] args) {
String s1 = "AB";
String s2 = new String("AB");
String s3 = "A";
String s4 = "B";
String s5 = "A" + "B";
String s6 = s3 + s4;
System.out.println(s1 == s2);
System.out.println(s1 == s5);
System.out.println(s1 == s6);
System.out.println(s1 == s6.intern());
System.out.println(s2 == s2.intern());
}

结果: 在这里插入图片描述 解析:真正理解此题目需要清楚以下三点 1)直接使用双引号声明出来的String对象会直接存储在常量池中; 2)String对象的intern方法会得到字符串对象在常量池中对应的引用,如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用; 3) 字符串的+操作其本质是创建了StringBuilder对象进行append操作,然后将拼接后的StringBuilder对象用toString方法处理成String对象,这一点可以用javap -c命令获得class文件对应的JVM字节码指令就可以看出来。


6.不可变对象String


String s = “Hello”; s = s+ “world !”;


代码执行后,原始的String对象中的内容到底变了没? 没有。因为String被设计为不可变(immutable)类,它的所有对象都是不可变对象。s原先指向一个String对象,内容是"Hello",然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个String对象,内容为"Hello world !",原来那个对象还在内存常量中,只是s这个引用变量不再指向它了。所以,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String类来代表字符串的话会引起很大的内存开销,应该考虑StringBuffer类,它允许修改


String类中有哪些常见的方法呢? 数组有没有length()这个方法?String有没有length()这个方法? 数组只有length属性,没有length方法,String有length方法。


一、String类对象的创建 字符串声明:String stringName; 字符串创建:stringName = new String(字符串常量);或stringName = 字符串常量;


二、String类构造方法 1、public String() 无参构造方法,用来创建空字符串的String对象。


String str1 = new String();

2、public String(String value) 用已知的字符串value创建一个String对象。


1 String str2 = new String("asdf"); 2 String str3 = new String(str2);

3、public String(char[] value) 用字符数组value创建一个String对象。


1 char[] value = {'a','b','c','d'};
2 String str4 = new String(value);//相当于String str4 = new String("abcd");

4、public String(char chars[], int startIndex, int numChars) 用字符数组chars的startIndex开始的numChars个字符创建一个String对象。


1 char[] value = {'a','b','c','d'};
2 String str5 = new String(value, 1, 2);//相当于String str5 = new String("bc");

5、public String(byte[] values) 用比特数组values创建一个String对象。


1 byte[] strb = new byte[]{65,66};
2 String str6 = new String(strb);//相当于String str6 = new String("AB");

三、String类常用方法 1、求字符串长度 public int length()//返回该字符串的长度 1 String str = new String("asdfzxc"); 2 int strlength = str.length();//strlength = 7


2、求字符串某一位置字符 public char charAt(int index)//返回字符串中指定位置的字符;注意字符串中第一个字符索引是0,最后一个是length()-1。 1 String str = new String("asdfzxc"); 2 char ch = str.charAt(4);//ch = z 3、提取子串 用String类的substring方法可以提取字符串中的子串,该方法有两种常用参数: 1)public String substring(int beginIndex)//该方法从beginIndex位置起,从当前字符串中取出剩余的字符作为一个新的字符串返回。 2)public String substring(int beginIndex, int endIndex)//该方法从beginIndex位置起,从当前字符串中取出到endIndex-1位置的字符作为一个新的字符串返回。


1 String str1 = new String("asdfzxc");
2 String str2 = str1.substring(2);//str2 = "dfzxc"
3 String str3 = str1.substring(2,5);//str3 = "dfz"

4、字符串比较 1)**public int compareTo(String anotherString)//**该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系。若当前对象比参数大则返回正整数,反之返回负整数,相等返回0。 2)public int compareToIgnore(String anotherString)//与compareTo方法相似,但忽略大小写。 3)public boolean equals(Object anotherObject)//比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false。 4)public boolean equalsIgnoreCase(String anotherString)//与equals方法相似,但忽略大小写。


> 1 String str1 = new String("abc"); 2 String str2 = new String("ABC");
> 3 int a = str1.compareTo(str2);//a>0 4 int b =
> str1.compareToIgnoreCase(str2);//b=0 5 boolean c =
> str1.equals(str2);//c=false 6 boolean d =
> str1.equalsIgnoreCase(str2);//d=true

5、字符串连接 public String concat(String str)//将参数中的字符串str连接到当前字符串的后面,效果等价于"+"。


1 String str = "aa".concat("bb").concat("cc");
2 相当于String str = "aa"+"bb"+"cc";

6、字符串中单个字符查找 1)public int indexOf(int ch/String str)//用于查找当前字符串中字符或子串,返回字符或子串在当前字符串中从左边起首次出现的位置,若没有出现则返回-1。 2)public int indexOf(int ch/String str, int fromIndex)//改方法与第一种类似,区别在于该方法从fromIndex位置向后查找。 3)public int lastIndexOf(int ch/String str)//该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找。 4)public int lastIndexOf(int ch/String str, int fromIndex)//该方法与第二种方法类似,区别于该方法从fromIndex位置向前查找。


1 String str = "I am a good student";
2 int a = str.indexOf('a');//a = 2
3 int b = str.indexOf("good");//b = 7
4 int c = str.indexOf("w",2);//c = -1
5 int d = str.lastIndexOf("a");//d = 5
6 int e = str.lastIndexOf("a",3);//e = 2

7、字符串中字符的大小写转换 1)public String toLowerCase()//返回将当前字符串中所有字符转换成小写后的新串 2)public String toUpperCase()//返回将当前字符串中所有字符转换成大写后的新串


1 String str = new String("asDF");
2 String str1 = str.toLowerCase();//str1 = "asdf"
3 String str2 = str.toUpperCase();//str2 = "ASDF"

8、字符串中字符的替换 1)public String replace(char oldChar, char newChar)//用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串。 2)public String replaceFirst(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的第一个和字符串regex相匹配的子串,应将新的字符串返回。 3)public String replaceAll(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的所有和字符串regex相匹配的子串,应将新的字符串返回。


1 String str = "asdzxcasd";
2 String str1 = str.replace('a','g');//str1 = "gsdzxcgsd"
3 String str2 = str.replace("asd","fgh");//str2 = "fghzxcfgh"
4 String str3 = str.replaceFirst("asd","fgh");//str3 = "fghzxcasd"
5 String str4 = str.replaceAll("asd","fgh");//str4 = "fghzxcfgh"

9、其他类方法 1)String trim()//截去字符串两端的空格,但对于中间的空格不处理。


1 String str = " a sd ";
2 String str1 = str.trim();
3 int a = str.length();//a = 6
4 int b = str1.length();//b = 4

2)boolean statWith(String prefix)或boolean endWith(String suffix)//用来比较当前字符串的起始字符或子字符串prefix和终止字符或子字符串suffix是否和当前字符串相同,重载方法中同时还可以指定比较的开始位置offset。


1 String str = "asdfgh"; 2 boolean a = str.statWith("as");//a = true 3 boolean b = str.endWith("gh");//b = true


3)regionMatches(boolean b, int firstStart, String other, int otherStart, int length)//从当前字符串的firstStart位置开始比较,取长度为length的一个子字符串,other字符串从otherStart位置开始,指定另外一个长度为length的字符串,两字符串比较,当b为true时字符串不区分大小写。 4)contains(String str)//判断参数s是否被包含在字符串中,并返回一个布尔类型的值。


1 String str = "student"; 2 str.contains("stu");//true 3 str.contains("ok");//false


5)String[] split(String str)//将str作为分隔符进行字符串分解,分解后的字字符串在字符串数组中返回。


1 String str = "asd!qwe|zxc#"; 2 String[] str1 = str.split("!|#");//str1[0] = "asd";str1[1] = "qwe";str1[2] = "zxc"; 四、字符串与基本类型的转换 1、字符串转换为基本类型 java.lang包中有Byte、Short、Integer、Float、Double类的调用方法: 1)public static byte parseByte(String s) 2)public static short parseShort(String s) 3)public static short parseInt(String s) 4)public static long parseLong(String s) 5)public static float parseFloat(String s) 6)public static double parseDouble(String s) 例如:


1 int n = Integer.parseInt("12"); 2 float f = Float.parseFloat("12.34"); 3 double d = Double.parseDouble("1.124");


2、基本类型转换为字符串类型 String类中提供了String valueOf()放法,用作基本类型转换为字符串类型。 1)static String valueOf(char data[]) 2)static String valueOf(char data[], int offset, int count) 3)static String valueOf(boolean b) 4)static String valueOf(char c) 5)static String valueOf(int i) 6)static String valueOf(long l) 7)static String valueOf(float f) 8)static String valueOf(double d) 例如:


1 String s1 = String.valueOf(12); 2 String s1 = String.valueOf(12.34); 3、进制转换 使用Long类中的方法得到整数之间的各种进制转换的方法: Long.toBinaryString(long l) Long.toOctalString(long l) Long.toHexString(long l) Long.toString(long l, int p)//p作为任意进制


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wym199606/article/details/112689213

随机推荐

2020年继电保护考试及继电保护模拟考试系统

题库来源:安全生产模拟考试一点通公众号小程序2020年继电保护考试及继电保护模拟考试系统,包含继电保护考试答案和解析及继电保护模拟考试系统练习。由安全生产模拟考试一点通公众...

如此,安好\已然,静舒 阅读(868)

一文带你快速上手Optional

1.前言相信不少小伙伴已经被java的NPE(NullPointerException)所谓的空指针异常搞的头昏脑涨,有大佬说过“防止NPE,是程序员的基本修养。”但是修养归修养...

热衷技术的Java程序员 阅读(139)

windows平台下python接口调用darknet训练的模型

windows平台下python接口调用darknet训练的模型进行视频中的目标检测的两种办法(方法2实践成功了,可以直接参考我的下一篇文章)。一个方法是可以直接用opencv的pyth...

是三水不是泗水 阅读(798)

Gym 101908

Gym-101908F-MusicFestival(树状数组+状压dp)在音乐节去观看节目表演。总共有n个舞台,每个舞台有Mi个show,...

Chloris F C 阅读(630)

ZJGSU OJ 2282 字符串排序(函数 && 指针)

几乎和2475那题一样题目描述编写一个程序,在主函数中输入n个不等长的字符串。用另一个函数sort()对它们排序,函数sort()原型为:voidsort(c...

AlanCorn_02 阅读(241)

Java习题10 多线程

1.下面方法中可以用来创建一个新线程的是(D)。A.实现java.lang.Runnable接口并重写run()方法B.实现java.lang.Runnable接口并重写start()方法C.实现ja...

小张学姐 阅读(546)

python工具包--Matplotlib

文章目录Matplotlib的基本操作风格设置条形图盒图直方图散点图3D图圆饼图布局设置模板引用示例1:Horizontalbarchart模板示例2:Creatingan...

Clichong 阅读(624)