【每日一题】Java 字符串(Part 2)相关面试题

问:下面程序的运行结果是什么?

 
 
  1. String stra = "ABC";

  2. String strb = new String("ABC");

  3. System.out.println(stra == strb);    //1,false

  4. System.out.println(stra.equals(strb));    //2,true


对于 1 和 2 中两个都是显式创建的新对象,使用 == 总是不等,String 的 equals 方法有被重写为值判断,所以 equals 是相等的。


 
 
  1. String str1 = "123";

  2. System.out.println("123" == str1.substring(0));    //3,true

  3. System.out.println("23" == str1.substring(1));    //4,false


对于 3 和 4 中 str1 的 substring 方法实现里面有个 index == 0 的判断,当 index 等于 0 就直接返回当前对象,否则新 new 一个 sub 的对象返回,而 == 又是地址比较,所以结果如注释。


 
 
  1. String str3 = new String("ijk");

  2. String str4 = str3.substring(0);

  3. System.out.println(str3 == str4);    //5,true

  4. System.out.println((new String("ijk") == str4));    //6,false


对于 5 和 6 来说没啥分析的必要了,参见上面对于 3 和 4 的分析结果。


 
 
  1. String str5 = "NPM";

  2. String str6 = "npm".toUpperCase();

  3. System.out.println(str5 == str6);    //7,false

  4. System.out.println(str5.equals(str6));    //8,true


  5. String str7 = new String("TTT");

  6. String str8 = "ttt".toUpperCase();

  7. System.out.println(str7 == str8);    //9,false

  8. System.out.println(str7.equals(str8));    //10,true


对于 7、8、9、10 来说实质都一样,toUpperCase 方法内部创建了新字符串对象。


 
 
  1. String str9 = "a1";

  2. String str10 = "a" + 1;

  3. System.out.println(str9 == str10);    //11,true


对于 11 来说当两个字符串常量连接时(相加)得到的新字符串依然是字符串常量且保存在常量池中只有一份。


 
 
  1. String str11 = "ab";

  2. String str12 = "b";

  3. String str13 = "a" + str12;

  4. System.out.println(str11 == str13);    //12,false


对于 12 来说当字符串常量与 String 类型变量连接时得到的新字符串不再保存在常量池中,而是在堆中新建一个 String 对象来存放,很明显常量池中要求的存放的是常量,有 String 类型变量当然不能存在常量池中了。


 
 
  1. String str14 = "ab";

  2. final String str15 = "b";

  3. String str16 = "a" + str15;

  4. System.out.println(str14 == str16);    //13,true


对于 13 来说此处是字符串常量与 String 类型常量连接,得到的新字符串依然保存在常量池中,因为对 final 变量的访问在编译期间都会直接被替代为真实的值。


 
 
  1. private static String getBB() {  

  2.    return "b";  

  3. }

  4. String str17 = "ab";  

  5. final String str18 = getBB();  

  6. String str19 = "a" + str18;  

  7. 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,原因同理。


 
 
  1. String str20 = "ab";

  2. String str21 = "a";  

  3. String str22 = "b";  

  4. String str23 = str21 + str22;  

  5. System.out.println(str23 == str20);    //15,false

  6. System.out.println(str23.intern() == str20);    //16,true

  7. System.out.println(str23 == str20.intern());    //17,false

  8. 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


这个帖子讨论的热火朝天,但是终归总结就下面这点有价值(版权声明,该结论归知乎该问题回答者所有):


1. 这种做法意义有多大?


如果没有及时清空而由 GC 来清除的话暴露窗口大约是秒这个数量级,如果能够在计算 HASH 后立即清除则暴露窗口大约是微秒数量级,如此简单的设计就可以降低如此多的被攻击概率,性价比是非常高的。


2. 如何使用反射来修改 String?和修改 char[] 相比有何区别和风险?


通过反射机制可以查看 String 内部的内存成员,从而可以直接修改其中的数据区,但是这样的做法会有问题,内部化的 String 为了提高 HASH 速度、节省空间会保证值相同的字符串通常只有一个实例,而 char[] 的修改是没有任何副作用的,但是 String 源码里面的 char[] 很可能是多个 String 共享的,我们改掉它就会殃及别的 String,譬如有一个密码是 "Password",而你密码框提示密码输入的文字也是 "Password",改掉第一个 "Password" 会把后面那个也改掉,所以修改 String 是有副作用的。


3. 如果一点明文也不想出现应该怎么做?


为了保证全部处理流程均无明文密码,需要底层 API 在给你密码之前就做了 HASH,并且这个 HASH 算法就是你想要的那种,最好还加盐,不过这只是在用户程序方面无明文,底层获取中会不会有明文就保证不了了。


4. 有没有绝对安全策略?


安全往往是相对于攻击成本而言的,攻击收益越高,黑客就越能接受攻击成本高的方案,因此你采取的安全策略应该与这个攻击收益相匹配,对于极其敏感和宝贵的数据来源就需要在安全方面上下很大功夫,目前来看没有绝对的安全,只有相对的安全。



问:用 java 代码实现字符串的反转?


答:这道题的答案很多,下面给出两种常见的答案。


使用 JDK 中 StringBuffer(并发安全)或者 StringBuilder 的反转方法,这是最好的办法,不仅速度快、效率高,代码如下:

 
 
  1. public String reverse(String str) {

  2.    if ((null == str) || (str.length() <= 1)) {

  3.        return str;

  4.    }

  5.    return new StringBuffer(str).reverse().toString();

  6. }


炫技能使用递归方案实现,代码如下:

 
 
  1. public String reverse(String str) {

  2.   if ((null == str) || (str.length()  <= 1)) {

  3.       return str;

  4.   }

  5.   return reverse(str.substring(1)) + str.charAt(0);

  6. }



问:用 java 代码来检查输入的字符串是否回文(对称)?


答:这道题的答案也有很多,下面给出两种常见的答案。


使用 JDK 现有 API 实现,代码如下:

 
 
  1. boolean isPalindrome(String str) {

  2.    if (str == null) {

  3.        return false;

  4.    }

  5.    StringBuilder strBuilder = new StringBuilder(str);

  6.    strBuilder.reverse();

  7.    return strBuilder.toString().equals(str);

  8. }


纯手撸写法实现,可以从 String 的两端比较下手,代码如下:

 
 
  1. boolean isPalindrome(String str) {

  2.    if (str == null) {

  3.        return false;

  4.    }

  5.    int length = str.length();

  6.    for (int i = 0; i < length / 2; i++) {

  7.        if (str.charAt(i) != str.charAt(length i 1)) {

  8.            return false;

  9.        }

  10.    }

  11.    return true;

  12. }


问:用 java 代码写一个方法从字符串中删除给定字符?


答:这道题太简单不过了,都有点不好意思放上来,不过为了照顾所有层次还是写下答案。

 
 
  1. String removeChar(String str, char c) {

  2.    if (str == null) {

  3.        return null;

  4.    }

  5.    return str.replaceAll(Character.toString(c), "");

  6. }

问:下面程序的运行结果是什么?

 
 
  1. String stra = "ABC";

  2. String strb = new String("ABC");

  3. System.out.println(stra == strb);    //1,false

  4. System.out.println(stra.equals(strb));    //2,true


对于 1 和 2 中两个都是显式创建的新对象,使用 == 总是不等,String 的 equals 方法有被重写为值判断,所以 equals 是相等的。


 
 
  1. String str1 = "123";

  2. System.out.println("123" == str1.substring(0));    //3,true

  3. System.out.println("23" == str1.substring(1));    //4,false


对于 3 和 4 中 str1 的 substring 方法实现里面有个 index == 0 的判断,当 index 等于 0 就直接返回当前对象,否则新 new 一个 sub 的对象返回,而 == 又是地址比较,所以结果如注释。


 
 
  1. String str3 = new String("ijk");

  2. String str4 = str3.substring(0);

  3. System.out.println(str3 == str4);    //5,true

  4. System.out.println((new String("ijk") == str4));    //6,false


对于 5 和 6 来说没啥分析的必要了,参见上面对于 3 和 4 的分析结果。


 
 
  1. String str5 = "NPM";

  2. String str6 = "npm".toUpperCase();

  3. System.out.println(str5 == str6);    //7,false

  4. System.out.println(str5.equals(str6));    //8,true


  5. String str7 = new String("TTT");

  6. String str8 = "ttt".toUpperCase();

  7. System.out.println(str7 == str8);    //9,false

  8. System.out.println(str7.equals(str8));    //10,true


对于 7、8、9、10 来说实质都一样,toUpperCase 方法内部创建了新字符串对象。


 
 
  1. String str9 = "a1";

  2. String str10 = "a" + 1;

  3. System.out.println(str9 == str10);    //11,true


对于 11 来说当两个字符串常量连接时(相加)得到的新字符串依然是字符串常量且保存在常量池中只有一份。


 
 
  1. String str11 = "ab";

  2. String str12 = "b";

  3. String str13 = "a" + str12;

  4. System.out.println(str11 == str13);    //12,false


对于 12 来说当字符串常量与 String 类型变量连接时得到的新字符串不再保存在常量池中,而是在堆中新建一个 String 对象来存放,很明显常量池中要求的存放的是常量,有 String 类型变量当然不能存在常量池中了。


 
 
  1. String str14 = "ab";

  2. final String str15 = "b";

  3. String str16 = "a" + str15;

  4. System.out.println(str14 == str16);    //13,true


对于 13 来说此处是字符串常量与 String 类型常量连接,得到的新字符串依然保存在常量池中,因为对 final 变量的访问在编译期间都会直接被替代为真实的值。


 
 
  1. private static String getBB() {  

  2.    return "b";  

  3. }

  4. String str17 = "ab";  

  5. final String str18 = getBB();  

  6. String str19 = "a" + str18;  

  7. 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,原因同理。


 
 
  1. String str20 = "ab";

  2. String str21 = "a";  

  3. String str22 = "b";  

  4. String str23 = str21 + str22;  

  5. System.out.println(str23 == str20);    //15,false

  6. System.out.println(str23.intern() == str20);    //16,true

  7. System.out.println(str23 == str20.intern());    //17,false

  8. 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


这个帖子讨论的热火朝天,但是终归总结就下面这点有价值(版权声明,该结论归知乎该问题回答者所有):


1. 这种做法意义有多大?


如果没有及时清空而由 GC 来清除的话暴露窗口大约是秒这个数量级,如果能够在计算 HASH 后立即清除则暴露窗口大约是微秒数量级,如此简单的设计就可以降低如此多的被攻击概率,性价比是非常高的。


2. 如何使用反射来修改 String?和修改 char[] 相比有何区别和风险?


通过反射机制可以查看 String 内部的内存成员,从而可以直接修改其中的数据区,但是这样的做法会有问题,内部化的 String 为了提高 HASH 速度、节省空间会保证值相同的字符串通常只有一个实例,而 char[] 的修改是没有任何副作用的,但是 String 源码里面的 char[] 很可能是多个 String 共享的,我们改掉它就会殃及别的 String,譬如有一个密码是 "Password",而你密码框提示密码输入的文字也是 "Password",改掉第一个 "Password" 会把后面那个也改掉,所以修改 String 是有副作用的。


3. 如果一点明文也不想出现应该怎么做?


为了保证全部处理流程均无明文密码,需要底层 API 在给你密码之前就做了 HASH,并且这个 HASH 算法就是你想要的那种,最好还加盐,不过这只是在用户程序方面无明文,底层获取中会不会有明文就保证不了了。


4. 有没有绝对安全策略?


安全往往是相对于攻击成本而言的,攻击收益越高,黑客就越能接受攻击成本高的方案,因此你采取的安全策略应该与这个攻击收益相匹配,对于极其敏感和宝贵的数据来源就需要在安全方面上下很大功夫,目前来看没有绝对的安全,只有相对的安全。



问:用 java 代码实现字符串的反转?


答:这道题的答案很多,下面给出两种常见的答案。


使用 JDK 中 StringBuffer(并发安全)或者 StringBuilder 的反转方法,这是最好的办法,不仅速度快、效率高,代码如下:

 
 
  1. public String reverse(String str) {

  2.    if ((null == str) || (str.length() <= 1)) {

  3.        return str;

  4.    }

  5.    return new StringBuffer(str).reverse().toString();

  6. }


炫技能使用递归方案实现,代码如下:

 
 
  1. public String reverse(String str) {

  2.   if ((null == str) || (str.length()  <= 1)) {

  3.       return str;

  4.   }

  5.   return reverse(str.substring(1)) + str.charAt(0);

  6. }



问:用 java 代码来检查输入的字符串是否回文(对称)?


答:这道题的答案也有很多,下面给出两种常见的答案。


使用 JDK 现有 API 实现,代码如下:

 
 
  1. boolean isPalindrome(String str) {

  2.    if (str == null) {

  3.        return false;

  4.    }

  5.    StringBuilder strBuilder = new StringBuilder(str);

  6.    strBuilder.reverse();

  7.    return strBuilder.toString().equals(str);

  8. }


纯手撸写法实现,可以从 String 的两端比较下手,代码如下:

 
 
  1. boolean isPalindrome(String str) {

  2.    if (str == null) {

  3.        return false;

  4.    }

  5.    int length = str.length();

  6.    for (int i = 0; i < length / 2; i++) {

  7.        if (str.charAt(i) != str.charAt(length i 1)) {

  8.            return false;

  9.        }

  10.    }

  11.    return true;

  12. }


问:用 java 代码写一个方法从字符串中删除给定字符?


答:这道题太简单不过了,都有点不好意思放上来,不过为了照顾所有层次还是写下答案。

 
 
  1. String removeChar(String str, char c) {

  2.    if (str == null) {

  3.        return null;

  4.    }

  5.    return str.replaceAll(Character.toString(c), "");

  6. }

猜你喜欢

转载自blog.csdn.net/durenniu/article/details/81063075