问:Java 的 == 与 equals 有什么区别?
答:对于 ==,如果作用于基本数据类型的变量,则直接比较其存储的值是否相等,如果作用于引用类型的变量,则比较的是所指向的对象的地址。
对于 equals 方法不能作用于基本数据类型的变量(即不存在基本类型.equals(xx)),如果没有对 equals 方法进行重写则比较的是引用类型的变量所指向的对象的地址,诸如 String、Date 等类对 equals 方法进行了重写的话比较的是所指向的对象内容。
问:下面程序的运行结果是什么?
String s1 = "abc";
StringBuffer s2 = new StringBuffer(s1);
System.out.println(s1.equals(s2)); //1,false
StringBuffer s3 = new StringBuffer("abc");
System.out.println(s3.equals("abc")); //2,false
System.out.println(s3.toString().equals("abc")); //3,true
答:注释 1 打印为 false,主要考察 String 的 equals 方法,String 源码中 equals 方法有对参数进行 instance of String 判断语句,StringBuffer 的祖先为 CharSequence,所以不相等;
注释 2 打印为 false,因为 StringBuffer 没有重写 Object 的 equals 方法,所以 Object 的 equals 方法实现是 == 判断,故为 false;
注释 3 打印为 true,因为 Object 的 toString 方法返回为 String 类型,String 重写了 equals 方法实现为值比较。
问:怎样将 GB2312 编码的字符串转换为 ISO-8859-1 编码的字符串?
答:如下代码即可实现,
String s1 = "你好";
String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");
问:String、StringBuffer、StringBuilder 的区别是什么?
答:
String 是字符串常量,StringBuffer 和 StringBuilder 都是字符串变量,后两者的字符内容可变,而前者创建后内容不可变;
StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,线程安全会带来额外的系统开销,所以 StringBuilder 的效率比 StringBuffer 高;
String 的每次修改操作都是在内存中重新 new 一个对象出来,而 StringBuffer、StringBuilder 则不用,且提供了一定的缓存功能,默认 16 个字节数组的大小,超过默认的数组长度时扩容为
原来字节数组的长度 * 2 + 2
,所以使用 StringBuffer 和 StringBuilder 时可以适当考虑下初始化大小,以便通过减少扩容次数来提高代码的高效性。
问:String 为什么是不可变的?
答:String 不可变是因为在 JDK 中 String 类被声明为一个 final 类,且类内部的 value 字节数组也是 final 的,只有当字符串是不可变时字符串池才有可能实现,字符串池的实现可以在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串;
如果字符串是可变的则会引起很严重的安全问题,譬如数据库的用户名密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中主机名和端口都是以字符串的形式传入,因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子改变字符串指向的对象的值造成安全漏洞;
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步,字符串自己便是线程安全的;因为字符串是不可变的所以在它创建的时候 hashcode 就被缓存了,不变性也保证了 hash 码的唯一性,不需要重新计算,这就使得字符串很适合作为 Map 的键,字符串的处理速度要快过其它的键对象,这就是 HashMap 中的键往往都使用字符串的原因。
问:说说 String str = "hello world"; 和 String str = new String("hello world"); 的区别?
答:在 java 的 class 文件中有专门的部分用来存储编译期间生成的字面常量和符号引用,这部分叫做 class 文件常量池,在运行期间对应着方法区的运行时常量池,所以 String str = "hello world";
在编译期间生成了字面常量和符号引用,运行期间字面常量 "hello world" 被存储在运行时常量池(只保存了一份),而通过 new 关键字来生成对象是在堆区进行的,堆区进行对象生成的过程是不会去检测该对象是否已经存在的,所以通过 new 来创建的一定是不同的对象,即使字符串的内容是相同的。
问:语句 String str = new String("abc"); 一共创建了多少个对象?
答:这个问题其实有歧义,但是很多公司爱在笔试题里面考察,非要是遇到了就答两个吧(一个是 “abc”,一个是指向 “abc” 的引用对象 str);之所以说有歧义是因为该语句在运行期间只创建了一个对象(堆上的 "abc" 对象),而在类加载过程中在运行时常量池中先创建了一个 "abc" 对象,运行期和类加载又是有区别的,所以这个题目的问法是有些不严谨的。因此这个问题如果换成 String str = new String("abc"); 涉及到几个 String 对象,则答案就是 2 个了。
问:下面程序的运行结果是什么?
String stra = "ABC";
String strb = new String("ABC");
System.out.println(stra == strb); //1,false
System.out.println(stra.equals(strb)); //2,true
对于 1 和 2 中两个都是显式创建的新对象,使用 == 总是不等,String 的 equals 方法有被重写为值判断,所以 equals 是相等的。
String str1 = "123";
System.out.println("123" == str1.substring(0)); //3,true
System.out.println("23" == str1.substring(1)); //4,false
对于 3 和 4 中 str1 的 substring 方法实现里面有个 index == 0 的判断,当 index 等于 0 就直接返回当前对象,否则新 new 一个 sub 的对象返回,而 == 又是地址比较,所以结果如注释。
String str3 = new String("ijk");
String str4 = str3.substring(0);
System.out.println(str3 == str4); //5,true
System.out.println((new String("ijk") == str4)); //6,false
对于 5 和 6 来说没啥分析的必要了,参见上面对于 3 和 4 的分析结果。
String str5 = "NPM";
String str6 = "npm".toUpperCase();
System.out.println(str5 == str6); //7,false
System.out.println(str5.equals(str6)); //8,true
String str7 = new String("TTT");
String str8 = "ttt".toUpperCase();
System.out.println(str7 == str8); //9,false
System.out.println(str7.equals(str8)); //10,true
对于 7、8、9、10 来说实质都一样,toUpperCase 方法内部创建了新字符串对象。
String str9 = "a1";
String str10 = "a" + 1;
System.out.println(str9 == str10); //11,true
对于 11 来说当两个字符串常量连接时(相加)得到的新字符串依然是字符串常量且保存在常量池中只有一份。
String str11 = "ab";
String str12 = "b";
String str13 = "a" + str12;
System.out.println(str11 == str13); //12,false
对于 12 来说当字符串常量与 String 类型变量连接时得到的新字符串不再保存在常量池中,而是在堆中新建一个 String 对象来存放,很明显常量池中要求的存放的是常量,有 String 类型变量当然不能存在常量池中了。
String str14 = "ab";
final String str15 = "b";
String str16 = "a" + str15;
System.out.println(str14 == str16); //13,true
对于 13 来说此处是字符串常量与 String 类型常量连接,得到的新字符串依然保存在常量池中,因为对 final 变量的访问在编译期间都会直接被替代为真实的值。
private static String getBB () {
return "b";
}
String str17 = "ab";
final String str18 = getBB();
String str19 = "a" + str18;
System.out.println(str17 == str19); //14,false
对于 14 来说 final String str18 = getBB() 其实与 final String str18 = new String(“b”) 是一样的,也就是说 return “b” 会在堆中创建一个 String 对象保存 ”b”,虽然 str18 被定义成了 final,但不代表是常量,因为虽然将 str18 用 final 修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此指向的不是同一个对象,所以并非定义为 final 的就保存在常量池中,很明显此处 str18 常量引用的 String 对象保存在堆中,因为 getBB() 得到的 String 已经保存在堆中了,final 的 String 引用并不会改变 String 已经保存在堆中这个事实;对于 str18 换成 final String str18 = new String("b"); 一样会返回 false,原因同理。
String str20 = "ab";
String str21 = "a";
String str22 = "b";
String str23 = str21 + str22;
System.out.println(str23 == str20); //15,false
System .out.println(str23.intern() == str20); //16,true
System .out.println(str23 == str20.intern()); //17,false
System .out.println(str23.intern() == str20.intern()); //18,true
对于 15 到 18 来说 str23 == str20 就是上面刚刚分析的,而对于调用 intern 方法如果字符串常量池中已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定)则返回字符串常量池中的字符串,否则将此 String 对象添加到字符串常量池中,并返回此 String 对象的引用,所以 str23.intern() == str20 实质是常量比较返回 true,str23 == str20.intern() 中 str23 就是上面说的堆中新对象,相当于一个新对象和一个常量比较,所以返回 false,str23.intern() == str20.intern() 就没啥说的了,指定相等。
答:结果见题目中注释部分,解析见上面分段说明,基于 JDK 1.7 版本分析。
注释 11 到 14 深刻的说明了我们在代码中使用 String 时应该留意的优化技巧。特别说明 String 的 + 和 += 在编译后实质被自动优化为了 StringBuilder 和 append 调用,但是如果在循环等情况下调用 + 或者 += 就是在不停的 new StringBuilder 对象 append 了,这是及其浪费的。
通过这道题说明要想玩明白 Java String 对象的核心其实就是玩明白字符串的堆栈和常量池,虚拟机为每个被装载的类型维护一个常量池,常量池就是该类型所用常量的一个有序集合,包括直接常量(String、Integer 和 Floating Point 常量)和对其他类型、字段和方法的符号引用,池中的数据项就像数组一样是通过索引访问的,由于常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在 Java 程序的动态链接中起着核心的作用。
问:为什么针对安全保密高的信息,char[] 比 String 更好?
答:因为 String 是不可变的,一旦创建就不能更改,直到垃圾收集器将它回收才能消失,即使我们修改了原先的变量,实际上也是在内存中新建一个对象,原数据还是保留在内存中等待回收;而字符数组 char[] 中的元素是可以更改的,也就是说像密码等保密信息用完之后我们可以马上修改它的值而不留痕迹,从而相对于 String 有更好的安全性。
关于这个问题知乎上有个热门的帖子:
https://www.zhihu.com/question/36734157/answer/68767786
这个帖子讨论的热火朝天,但是终归总结就下面这点有价值(版权声明,该结论归知乎该问题回答者所有):
- 这种做法意义有多大?
如果没有及时清空而由 GC 来清除的话暴露窗口大约是秒这个数量级,如果能够在计算 HASH 后立即清除则暴露窗口大约是微秒数量级,如此简单的设计就可以降低如此多的被攻击概率,性价比是非常高的。
- 如何使用反射来修改 String?和修改 char[] 相比有何区别和风险?
通过反射机制可以查看 String 内部的内存成员,从而可以直接修改其中的数据区,但是这样的做法会有问题,内部化的 String 为了提高 HASH 速度、节省空间会保证值相同的字符串通常只有一个实例,而 char[] 的修改是没有任何副作用的,但是 String 源码里面的 char[] 很可能是多个 String 共享的,我们改掉它就会殃及别的 String,譬如有一个密码是 "Password",而你密码框提示密码输入的文字也是 "Password",改掉第一个 "Password" 会把后面那个也改掉,所以修改 String 是有副作用的。
- 如果一点明文也不想出现应该怎么做?
为了保证全部处理流程均无明文密码,需要底层 API 在给你密码之前就做了 HASH,并且这个 HASH 算法就是你想要的那种,最好还加验,不过这只是在用户程序方面无明文,底层获取中会不会有明文就保证不了了。
- 有没有绝对安全策略?
安全往往是相对于攻击成本而言的,攻击收益越高,黑客就越能接受攻击成本高的方案,因此你采取的安全策略应该与这个攻击收益相匹配,对于极其敏感和宝贵的数据来源就需要在安全方面上下很大功夫,目前来看没有绝对的安全,只有相对的安全。
问:用 java 代码实现字符串的反转?
答:这道题的答案很多,下面给出两种常见的答案。
使用 JDK 中 StringBuffer(并发安全)或者 StringBuilder 的反转方法,这是最好的办法,不仅速度快、效率高,代码如下:
public String reverse (String str ){
if ((null == str) || (str.length() <= 1)) {
return str;
}
return new StringBuffer(str).reverse().toString();
}
优雅使用递归方案实现,代码如下:
public String reverse (String str ){
if ((null == str) || (str.length() <= 1)) {
return str;
}
return reverse(str.substring(1)) + str.charAt(0);
}
问:用 java 代码来检查输入的字符串是否回文(对称)?
答:这道题的答案也有很多,下面给出两种常见的答案。
使用 JDK 现有 API 实现,代码如下:
boolean isPalindrome (String str ){
if (str == null) {
return false;
}
int length = str.length();
for (int i = 0; i < length / 2; i++) {
if (str.charAt(i) != str.charAt(length – i – 1)) {
return false;
}
} return true;
}
问:用 java 代码写一个方法从字符串中删除给定字符?
答:
String removeChar (String str ,char c ){
if (str == null) {
return null;
}
return str.replaceAll(Character.toString(c), "");
}