String常量池的面试相关内容

String
字符串的不可变性
定义一个字符串
String s = "abcd";
 
s 中保存了 string 对象的引用。下面的箭头可以理解为“存储他的引用”。
使用变量来赋值变量
String s2 = s;
 
s2 保存了相同的引用值,因为他们代表同一个对象。
字符串连接
s = s.concat("ef");
 
s 中保存的是一个重新创建出来的 string 对象的引用。
总结
一旦一个 string 对象在内存(堆)中被创建出来,他就无法被修改。特别要注意的是,
String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
如果你需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的 string 对象被创建出来。
JDK 6 和 JDK 7 中 substring 的原理及区别
String 是 Java 中一个比较基础的类,每一个开发人员都会经常接触到。而且, String 也是面试中经常会考的知识点。String 有很多方法,有些方法比较常用,有些方法不太常用。今天要介绍的 subString 就是一个比较常用的方法,而且围绕 subString 也有很多面试题。
substring(int beginIndex, int endIndex)
substring(int beginIndex, int endIndex)
方法在不同版本的 JDK 中的实现是不同的。了解他们的区别可以帮助你更好的使用他。为简单起见,后文中用 substring()代表方法。
substring() 的作用
substring(int beginIndex,    int    endIndex)
方法截取字符串并返回其[beginIndex, endIndex-1]范围内的内容。
String x = "abcdef"; x = x.substring(1,3);
System.out.println(x);
输出内容:
bc
调用 substring()时发生了什么?
你可能知道,因为 x 是不可变的,当使用 x.substring(1,3)对 x 赋值的时候,它会指向一个全新的字符串:
 
然而,这个图不是完全正确的表示堆中发生的事情。因为在 jdk6 和 jdk7 中调用 substring 时发生的事情并不一样。
JDK 6 中的 substring
    String 是通过字符数组实现的。在 jdk    6    中,String 类包含三个成员变量:
char value[]    ,    int offset    ,    int count
。他们分别用来存储真正的字符数组,数组的第一个位置索引以及字符串中包含的字符个数。
当调用 substring 方法的时候,会创建一个新的 string 对象,但是这个 string 的值仍然指向堆中的同一个字符数组。这两个对象中只有 count 和 offset 的值是不同的。
 
下面是证明上说观点的 Java 源码中的关键代码:
//JDK 6
String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count;
}
public String substring(int beginIndex, int endIndex) {
//check boundary
return new String(offset + beginIndex, endIndex - beginIndex, value); }
JDK 6 中的 substring 导致的问题
如果你有一个很长很长的字符串,但是当你使用 substring 进行切割的时候你只需要很短的一段。这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。在 JDK 6 中,一般用以下方式来解决该问题,原理其实就是生成一个新的字符串并引用他。
x = x.substring(x, y) + ""
关于 JDK 6 中 subString 的使用不当会导致内存系列已经被官方记录在 Java Bug
Database 中:
 
内存泄露:在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。 内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
JDK 7 中的 substring
上面提到的问题,在 jdk 7 中得到解决。在 jdk 7 中,substring 方法会在堆内存中创建一个新的数组。
 
Java 源码中关于这部分的主要代码如下:
//JDK 7
public String(char value[], int offset, int count) {
//check boundary
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
public String substring(int beginIndex, int endIndex) {
//check boundary
int subLen = endIndex - beginIndex;
return new String(value, beginIndex, subLen); }
new String
以上是 JDK 7 中的 subString 方法,其使用创建了一个新字符串,避免对老字符串的引用。从而解决了内存泄露问题。
所以,如果你的生产环境中使用的 JDK 版本小于 1.7,当你使用 String 的 subString 方法时一定要注意,避免内存泄露。
replaceFirst、replaceAll、replace 区别
replace、replaceAll 和 replaceFirst 是 Java 中常用的替换字符的方法,它们的方法定义是:
    replace(CharSequence    target,    CharSequence    replacement)    ,用
replacement 替换所有的 target,两个参数都是字符串。
    replaceAll(String regex,    String replacement) ,用 replacement 替换所有的
regex 匹配项,regex 很明显是个正则表达式,replacement 是字符串。
replaceFirst(String regex, String replacement) ,基本和 replaceAll 相同,区别是只替换第一个匹配项。
可以看到,其中 replaceAll 以及 replaceFirst 是和正则表达式有关的,而 replace 和正则表达式无关。
replaceAll 和 replaceFirst 的区别主要是替换的内容不同,replaceAll 是替换所有匹
配的字符,而 replaceFirst()仅替换第一次出现的字符。
用法例子
以下例子参考:http://www.51gjie.com/java/771.html
1、    replaceAll() 替换符合正则的所有文字
//文字替换(全部)
Pattern pattern = Pattern.compile("正则表达式");
Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World
");
//替换第一个符合正则的数据
System.out.println(matcher.replaceAll("Java"));
2、    replaceFirst() 替换第一个符合正则的数据
//文字替换(首次出现字符)
Pattern pattern = Pattern.compile("正则表达式");
Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World
");
//替换第一个符合正则的数据
System.out.println(matcher.replaceFirst("Java"));
 
71
3、    replaceAll()替换所有 html 标签
//去除html标记
Pattern pattern = Pattern.compile("<.+?>", Pattern.DOTALL); Matcher matcher = pattern.matcher("<a href=\"index.html\">主页</a>");
String string = matcher.replaceAll(""); System.out.println(string);
4、    replaceAll() 替换指定文字
//替换指定{}中文字
String str = "Java目前的发展史是由{0}年-{1}年";
String[][] object = { new String[] {
"\\{0\\}",
"1995"
}, new String[] {
"\\{1\\}",
"2007"
}
};
System.out.println(replace(str, object)); public static String replace(final String sourceString, Object[] object) { String temp = sourceString; for (int i = 0; i < object.length; i++) {
String[] result = (String[]) object[i];
Pattern pattern = Pattern.compile(result[0]); Matcher matcher = pattern.matcher(temp); temp = matcher.replaceAll(result[1]);
}
return temp;
}
5、    replace()替换字符串
System.out.println("abac".replace("a", "\a")); //\ab\ac
String 对“+”的重载
1、    String s = "a" + "b",编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),即变成 String s = "ab"
2、    对于能够进行优化的(String s = "a" + 变量 等)用 StringBuilder 的 append() 方法替代,最后调用 toString() 方法 (底层就是一个 new String())
字符串拼接的几种方式和区别
字符串,是 Java 中最常用的一个数据类型了。
本文,也是对于 Java 中字符串相关知识的一个补充,主要来介绍一下字符串拼接相关的知识。本文基于 jdk1.8.0_181。
字符串拼接
字符串拼接是我们在 Java 代码中比较经常要做的事情,就是把多个字符串拼接到一起。
我们都知道,String 是 Java 中一个不可变的类,所以他一旦被实例化就无法被修改。
不可变类的实例一旦创建,其成员变量的值就不能被修改。这样设计有很多好处,比如可以缓存 hashcode、使用更加便利以及更加安全等。
但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?
字符串不变性与字符串拼接
其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:
 
其实最后我们得到的 s 已经是一个新的字符串了。如下图:
 
s 中保存的是一个重新创建出来的 String 对象的引用。
那么,在 Java 中,到底如何进行字符串拼接呢?字符串拼接有很多种方式,这里简单介绍几种比较常用的。
使用+拼接字符串
在 Java 中,拼接字符串最简单的方式就是直接使用符号+来拼接。如:
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat + "," + introduce;
这里要特别说明一点,有人把 Java 中使用+拼接字符串的功能理解为运算符重载。其实并不是,Java 是不支持运算符重载的。这其实只是 Java 提供的一个语法糖。后面再详细介绍。
运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
Concat
除了使用+拼接字符串之外,还可以使用 String 类中的方法 concat 方法来拼接字符串。如:

String wechat = "Hollis";
    

String introduce = "每日更新Java相关技术文章";
    
String hollis = wechat.concat(",").concat(introduce);    
StringBuffer
关于字符串,Java 中除了定义了一个可以用来定义字符串常量的 String 类以外,还
提供了可以用来定义字符串变量的 StringBuffer 类,它的对象是可以扩充和修改的。使用 StringBuffer 可以方便的对字符串进行拼接。如:

StringBuffer wechat = new StringBuffer("Hollis");
    

String introduce = "每日更新Java相关技术文章";
        
StringBuffer hollis = wechat.append(",").append(introduce);    
StringBuilder
除了 StringBuffer 以外,还有一个类 StringBuilder 也可以使用,其用法和
StringBuffer 类似。如:

StringBuilder wechat = new StringBuilder("Hollis");
    

String introduce = "每日更新Java相关技术文章";
        
StringBuilder hollis = wechat.append(",").append(introduce);    
StringUtils.join
除了 JDK 中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如 apache.commons 中提供的 StringUtils 类,其中的 join 方法可以拼接字符串。
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
System.out.println(StringUtils.join(wechat, ",", introduce));
这里简单说一下,StringUtils 中提供的 join 方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:
String []list ={"Hollis","每日更新Java相关技术文章"};
String result= StringUtils.join(list,",");
System.out.println(result);
//结果:Hollis,每日更新Java相关技术文章
并 且 , J a v a 8 中 的 S t r i n g 类 中 也 提 供 了 一 个 静 态 的 j o i n 方 法 , 用 法 和
StringUtils.join 类似。
以上就是比较常用的五种在 Java 种拼接字符串的方式,那么到底哪种更好用呢?为什么 Java 开发手册中不建议在循环体中使用+进行字符串拼接呢?
 
使用+拼接字符串的实现原理
前面提到过,使用+拼接字符串,其实只是 Java 提供的一个语法糖, 那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。
还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat + "," + introduce;
反编译后的内容如下,反编译工具为 jad。

String wechat = "Hollis";
    

String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6


587\u7AE0";//每日更新Java相关技术文章
    

String hollis = (new StringBuilder()).append(wechat).append(",").append(int

roduce).toString();    
通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将
String 转成了 StringBuilder 后,使用其 append 方法进行处理的。
    那 么 也 就 是 说 , J a v a    中 的 + 对 字 符 串 的 拼 接 , 其 实 现 原 理 是 使 用
StringBuilder.append。
concat 是如何实现的
我们再来看一下 concat 方法的源代码,看一下这个方法又是如何实现的。

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);
        
    }        
这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的 String 对象并返回。
通过源码我们也可以看到,经过 concat 方法,其实是 new 了一个新的 String,这也就呼应到前面我们说的字符串的不变性问题上了。
StringBuffer 和 StringBuilder
接下来我们看看 StringBuffer 和 StringBuilder 的实现原理。
和 String 类类似,StringBuilder 类也封装了一个字符数组,定义如下:
char[] value;    
与 String 不同的是,它并不是 final 的,所以他是可以修改的。另外,与 String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:
int count;    
其 append 源码如下:
 
该类继承了 AbstractStringBuilder 类,看下其 append 方法:
 
append 会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。
StringBuffer 和 StringBuilder 类似,最大的区别就是 StringBuffer 是线程安全的,看一下 StringBuffer 的 append 方法。
 
StringBuilder
该方法使用 synchronized 进行声明,说明是一个线程安全的方法。而则不是线程安全的。
StringUtils.join 是如何实现的
StringBuilder
通过查看 StringUtils.join 的源代码,我们可以发现,其实他也是通过来实现的。
 
效率比较
既然有这么多种字符串拼接的方法,那么到底哪一种效率最高呢?我们来简单对比一下。
 
我们使用形如以上形式的代码,分别测试下五种字符串拼接代码的运行时间。得到结果如下:
 
从结果可以看出,用时从短到长的对比是:
StringBuilder<StringBuffer<concat<+<StringUtils.join
StringBuffer 在 StringBuilder 的基础上,做了同步处理,所以在耗时上会相对多一些。
StringUtils.join 也是使用了 StringBuilder,并且其中还是有很多其他操作,所以耗时较长,这个也容易理解。其实 StringUtils.join 更擅长处理字符串数组或者列表的拼接。
那么问题来了,前面我们分析过,其实使用+拼接字符串的实现原理也是使用的
StringBuilder,那为什么结果相差这么多,高达 1000 多倍呢?
我们再把以下代码反编译下:

long t1 = System.currentTimeMillis();
    

String str = "hollis";
        

for (int i = 0; i < 50000; i++) {
        

String s = String.valueOf(i);
        

str += s;
            
            

long t2 = System.currentTimeMillis();
    
System.out.println("+ cost:" + (t2 - t1));    
}
反编译后代码如下:
 
我们可以看到反编译后的代码,在 for 循环中,每次都是 new 了一个 StringBuilder,然后再把 String 转成 StringBuilder,再进行 append。
而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。
StringBuilder
append
所以, Java 开发手册建议:循环体内,字符串的连接方式,使用的方法进行扩展。而不要使用+。
总结
本文介绍了什么是字符串拼接,虽然字符串是不可变的,但是还是可以通过新建字符串的方式来进行字符串的拼接。
常用的字符串拼接方式有五种,分别是使用+、使用 concat、使用 StringBuilder、使用 StringBuffer 以及使用 StringUtils.join。
由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。
因此,经过对比,我们发现,直接使用 StringBuilder 的方式是效率最高的。因为
StringBuilder 天生就是设计来定义可变字符串和字符串的变化操作的。
但是,还要强调的是:
1、如果不是在循环体中进行字符串拼接的话,直接使用+就好了。
2 、 如 果 在 并 发 场 景 中 进 行 字 符 串 拼 接 的 话 , 要 使 用 S t r i n g B u f f e r 来 代 替
StringBuilder。
String.valueOf 和 Integer.toString 的区别
我们有三种方式将一个 int 类型的变量变成呢过 String 类型,那么他们有什么区别?
1.int i = 5;
2.String i1 = "" + i;
3.String i2 = String.valueOf(i);
4.String i3 = Integer.toString(i);
第三行和第四行没有任何区别,因为 String.valueOf(i)也是调用 Integer.toString(i) 来实现的。
第二行代码其实是 String i1 = (new StringBuilder()).append(i).toString();,首先
创建一个 StringBuilder 对象,然后再调用 append 方法,再调用 toString 方法。 switch 对 String 的支持
Java 7 中,switch 的参数可以是 String 类型了,这对我们来说是一个很方便的改进。到目前为止 switch 支持这样几种数据类型:byte 、short、int 、char 、String 。但是,作为一个程序员我们不仅要知道他有多么好用,还要知道它是如何实现的,switch 对整型的支持是怎么实现的呢?对字符型是怎么实现的呢?String 类型呢?有一点 Java 开发经验的人这个时候都会猜测 switch 对 String 的支持是使用 equals()方法和 hashcode()方法。那么到底是不是这两个方法呢?接下来我们就看一下,switch 到底是如何实现的。一、switch 对整型支持的实现
下面是一段很简单的 Java 代码,定义一个 int 型变量 a,然后使用 switch 语句进行判断。执行这段代码输出内容为 5,那么我们将下面这段代码反编译,看看他到底是怎么实现的。
public class switchDemoInt {
public static void main(String[] args) { int a = 5; switch (a) { case 1:
System.out.println(1); break;
case 5:
System.out.println(5); break; default:
break;
}
}}
//output 5
反编译后的代码如下:
public class switchDemoInt
{ public switchDemoInt()
{ }
public static void main(String args[])
{
int a = 5;
switch(a)
{
case 1: // '\001'
System.out.println(1); break;
case 5: // '\005'
System.out.println(5); break;
}
}
}
我们发现,反编译后的代码和之前的代码比较除了多了两行注释以外没有任何区别,那么我们就知道,switch 对 int 的判断是直接比较整数的值。
二、    switch 对字符型支持的实现
直接上代码:
public class switchDemoInt { public static void main(String[] args) { char a = 'b'; switch (a) { case 'a':
System.out.println('a'); break;
case 'b':
System.out.println('b'); break; default:
break;
}
}
}
 
编译后的代码如下:
public class switchDemoChar
{ public switchDemoChar()
{ }
public static void main(String args[])
{ char a = 'b'; switch(a)
{
case 97: // 'a'
System.out.println('a'); break;
case 98: // 'b'
System.out.println('b'); break;
}
}
}
通过以上的代码作比较我们发现:对 char 类型进行比较的时候,实际上比较的是 ascii 码,编译器会把 char 型变量转换成对应的 int 型变量。
三、    switch 对字符串支持的实现
还是先上代码:
public class switchDemoString { public static void main(String[] args) {
String str = "world"; switch (str) { case "hello":
System.out.println("hello"); break;
case "world":
System.out.println("world"); break; default:
break;
}
}
}
对代码进行反编译:
public class switchDemoString
{ public switchDemoString()
{ }
public static void main(String args[])
{
String str = "world";
String s;
switch((s = str).hashCode())
{ default:
break;
case 99162322:
if(s.equals("hello"))
System.out.println("hello"); break;
case 113318802:
if(s.equals("world"))
System.out.println("world"); break;
}
}
}
hashCod
看到这个代码,你知道原来字符串的 switch 是通过 equals()和 hashCode()方法来实现的。记住,switch 中只能使用整型,比如 byte,short,char(ackii 码是整型)以及 int。还好 hashCode()方法返回的是 int,而不是 long。通过这个很容易记住e 返回的是 int 这个事实。仔细看下可以发现,进行 switch 的实际是哈希值,然后通过使用 equals 方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行 switch 或者使用纯整数常量,但这也不是很差。因为 Java 编译器只增加了一个 equals 方法,如果你比较的是字符串字面量的话会非常快,比如”abc” ==”abc”。如果你把 hashCode()方法的调用也考虑进来了,那么还会再多一次的调用开销,因为字符串一旦创建了,它就会把哈希值缓存起来。因此如果这个 switch 语句是用在一个循环里的,比如逐项处理某个值,或者游戏引擎循环地渲染屏幕,这里 hashCode() 方法的调用开销其实不会很大。
好,以上就是关于 switch 对整型、字符型、和字符串型的支持的实现方式,总结一下我们可以发现,其实 switch 只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后在使用 switch 的。
字符串池
字符串大家一定都不陌生,他是我们非常常用的一个类。
String 作为一个 Java 类,可以通过以下两种方式创建一个字符串:
String str = "Hollis";
String str = new String("Hollis");
而第一种是我们比较常用的做法,这种形式叫做"字面量"。
在 JVM 中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池。
当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。
这种机制,就是字符串驻留或池化。
字符串常量池的位置
在 JDK 7 以前的版本中,字符串常量池是放在永久代中的。
因为按照计划,JDK 会在后续的版本中通过元空间来代替永久代,所以首先在 JDK
7 中,将字符串常量池先从永久代中移出,暂时放到了堆内存中。
在 JDK 8 中,彻底移除了永久代,使用元空间替代了永久代,于是字符串常量池再次从堆内存移动到永久代中。
 

Guess you like

Origin blog.csdn.net/qq_25580555/article/details/120043019