Java 字符串

问: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

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

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

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

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

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

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

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

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

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

问:用 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), "");
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343