Java面试炼金系列 (1) | 关于String类的常见面试题剖析

文章以及源代码已被收录到:https://github.com/mio4/Java-Gold

0x0 基础知识

1. '==' 运算符

Java中的数据类型分为基本数据类型和引用数据类型:

  1. 基本类型:编程语言中内置的最小粒度的数据类型。它包括四大类八种类型

    • 4种整数类型:byteshortintlong
    • 2种浮点数类型:floatdouble
    • 1种字符类型:char
    • 1种布尔类型:boolean
  2. 引用类型:引用也叫句柄,引用类型,是编程语言中定义的在句柄中存放着实际内容所在地址的地址值的一种数据形式,例如:
    • 接口
    • 数组
  • 对于基本类型来说,== 比较的是它们的值
  • 对于引用类型来说,== 比较的是它们在内存中存放的地址(堆内存地址)

举例说明:

  1. public static void main(String[] args) {
  2. //基本数据类型
  3. int num1 = 100;
  4. int num2 = 100;
  5. System.out.println("num1 == num2 : " + (num1 == num2) + "\n");
  6. //引用类型,其中'System.identityHashCode'可以理解为打印对象地址
  7. String str1 = "mio4";
  8. String str2 = "mio4";
  9. System.out.println("str1 address : " + System.identityHashCode(str1));
  10. System.out.println("str2 address : " + System.identityHashCode(str1));
  11. System.out.println("str1 == str2 : " + (str1 == str2) + "\n");
  12. String str3 = new String("mio4");
  13. String str4 = new String("mio4");
  14. System.out.println("str3 address : " + System.identityHashCode(str3));
  15. System.out.println("str4 address : " + System.identityHashCode(str4));
  16. System.out.println("str3 == str4 : " + (str3 == str4));
  17. }

运行上面的代码,可以得到以下结果:

  1. num1 == num2 : true
  2. str1 address : 1639705018
  3. str2 address : 1639705018
  4. str1 == str2 : true
  5. str3 address : 1627674070
  6. str4 address : 1360875712
  7. str3 == str4 : false

可以看到str1和str2的内存地址都是1639705018,所以使用==判断为true,

但是str3和str4的地址是不同的,所以判断为false

2. equals()方法

2.1 Object类equals()

在Java语言中,所有类都是继承于Object这个超类的,在这个类中也有一个equals()方法,那么我们先来看一下这个方法。

可以看得出,这个方法很简单,就是比较对象的内存地址的。所以在对象没有重写这个方法时,默认使用此方法,即比较对象的内存地址值。但是类似于String、Integer等类均已重写了equals()。下面以String为例。

2.2 String类equals()

很明显,String的equals()方法仅仅是对比它的 数据值,而不是对象的 内存地址

String 为例测试一下:

  1. public static void main(String[] args) {
  2. String str1 = "mio4";
  3. String str2 = "mio4";
  4. String str3 = new String("mio4");
  5. String str4 = new String("mio4");
  6. System.out.println("str1 address : " + System.identityHashCode(str1));
  7. System.out.println("str2 address : " + System.identityHashCode(str1));
  8. System.out.println("str1.equals(str2) : " + str1.equals(str2) + "\n");
  9. System.out.println("str3 address : " + System.identityHashCode(str3));
  10. System.out.println("str4 address : " + System.identityHashCode(str4));
  11. System.out.println("str3.equals(str4) : " + str3.equals(str4) + "\n");
  12. }

测试输出为如下,可以看出str3str4地址不同,但是因为String字符串内容相同,所以equals判断为true

  1. str1 address : 1639705018
  2. str2 address : 1639705018
  3. str1.equals(str2) : true
  4. str3 address : 1627674070
  5. str4 address : 1360875712
  6. str3.equals(str4) : true

3. hashCode()方法

3.1 为啥有这个方法?使用场景

Java中的集合(Collection)有三类,一类是List,一类是Queue,集合内的元素是有序的,元素可以重复;再有一类就是Set,一个集合内的元素无序,但元素不可重复。

  • 那么, 这里就有一个比较严重的问题:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢? 这就是 Object.equals 方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。 也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。于是,Java采用了哈希表的原理。 这样,我们对每个要存入集合的元素使用哈希算法算出一个值,然后根据该值计算出元素应该在数组的位置。所以,当集合要添加新的元素时,可分为两个步骤:   

    • 先调用这个元素的 hashCode 方法,然后根据所得到的值计算出元素应该在数组的位置。如果这个位置上没有元素,那么直接将它存储在这个位置上;
    • 如果这个位置上已经有元素了,那么调用它的equals方法与新元素进行比较:相同的话就不存了,否则,将其存在这个位置对应的链表中(Java 中 HashSet, HashMap 和 Hashtable的实现总将元素放到链表的表头)。

3.2 hashCode()和equals()关联

 前提: 谈到hashCode就不得不说equals方法,二者均是Object类里的方法。由于Object类是所有类的基类,所以一切类里都可以重写这两个方法。

  • 原则 1 : 如果 x.equals(y) 返回 “true”,那么 x 和 y 的 hashCode() 必须相等 ;
  • 原则 2 : 如果 x.equals(y) 返回 “false”,那么 x 和 y 的 hashCode() 有可能相等,也有可能不等 ;
  • 原则 3 : 如果 x 和 y 的 hashCode() 不相等,那么 x.equals(y) 一定返回 “false” ;
  • 原则 4 : 一般来讲,equals 这个方法是给用户调用的,而 hashcode 方法一般用户不会去调用 ;
  • 原则 5 : 当一个对象类型作为集合对象的元素时,那么这个对象应该拥有自己的equals()和hashCode()设计,而且要遵守前面所说的几个原则。

总结来说,需要注意的是:

  • equals相等的两个对象,hashCode一定相等
  • equals方法不相等的两个对象,hashCode有可能相等

0x1 高频面试题

1. 看过String源码吗?为啥用final修饰?

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {}

核心解释:

1.为了实现字符串池

2.为了线程安全

3.为了实现String可以创建HashCode不可变性

  • final修饰的String,代表了String的不可继承性,final修饰的char[]代表了被存储的数据不可更改性。但是:虽然final代表了不可变,但仅仅是引用地址不可变,并不代表了数组本身不会变。
  • final也可以将数组本身改变的,这个时候,起作用的还有private,正是因为两者保证了String的不可变性。
  • 那么为什么保证String不可变呢,因为只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String.intern()将不能实现,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
  • 因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

2. String有哪些初始化方式?

String类型的初始化在Java中分为两类:

  • 一类是通过双引号包裹一个字符来初始化;
  • 另一类是通过关键字new像一个普通的对象那样初始化一个String实例。

前者在常量池l中开辟一个常量,并返回相应的引用,而后者是在堆中开辟一个常量,再返回相应的对象。所以,两者的reference肯定是不同的:

  1. public static void main(String... args) {
  2. String s1 = "abcd";
  3. String s2 = new String("abcd");
  4. System.out.println(s1 == s2); // false
  5. }

而常量池中的常量是可以被共享用于节省内存开销和创建时间的开销(这也是引入常量池的原因),例如:

  1. public static void main(String... args) {
  2. String s1 = "abcd";
  3. String s2 = "abcd";
  4. System.out.println(s1 == s2); // true
  5. }

结合这两者,其实还可以回答另外一个常见的面试题目:

  1. public static void main(String... args) {
  2. String s = new String("abcd");
  3. }

这句话创建了几个对象?

首先毫无疑问,"abcd"本身是一个对象,被放于常量池。而由于这里使用了new关键字,所以s得到的对象必然是被创建在heap里的。所以,这里其实一共创建了2个对象。

需要注意的是,如果在这个函数被调用前的别的地方,已经有了"abcd"这个字符串,那么它就事先在常量池中被创建了出来。此时,这里就只会创建一个对象,即创建在heap的new String("abcd")对象。

3. String是线程安全的吗?

String是不可变类,一旦创建了String对象,我们就无法改变它的值。因此,它是线程安全的,可以安全地用于多线程环境中。

4. 为什么我们在使用HashMap的时候常用String做key?

因为字符串是不可变的,当创建字符串时,它的它的hashcode被缓存下来,不需要再次计算。因为HashMap内部实现是通过key的hashcode来确定value的存储位置,所以相比于其他对象更快。这也是为什么我们平时都使用String作为HashMap对象。

5. String的intern()方法是什么?

String.intern()方法,可以在runtime期间将常量加入到常量池(constant pool)。它的运作方式是:

  1. 如果constant pool中存在一个常量恰好等于这个字符串的值,则intern()方法返回这个存在于constant pool中的常量的引用。
  2. 如果constant pool不存在常量恰好等于这个字符串的值,则在constant pool中创建一个新的常量,并将这个字符串的值赋予这个新创建的在constant pool中的常量。intern()方法返回这个新创建的常量的引用。

示例:

  1. public static void main(String... args) {
  2. String s1 = "abcd";
  3. String s2 = new String("abcd");
  4. /**
  5. * s2.intern() will first search String constant pool,
  6. * of which the value is the same as s2.
  7. */
  8. String s3 = s2.intern();
  9. // As s1 comes from constant pool, and s3 is also comes from constant pool, they're same.
  10. System.out.println(s1 == s3);
  11. // As s2 comes from heap but s3 comes from constant pool, they're different.
  12. System.out.println(s2 == s3);
  13. }
  14. /**
  15. * Output:
  16. * true
  17. * false
  18. */

回顾到最开始的第一部分,为什么要引入intern()这个函数呢?就是因为,虽然"abcd"是被分配在常量池里的,但是,一旦使用new String("abcd")就会在heap中新创建一个值为abcd的对象出来。试想,如果有100个这样的语句,岂不是就要在heap里创建100个同样值的对象?!这就造成了运行的低效和空间的浪费。

于是,如果引入了intern()它就会直接去常量池找寻是否有值相同的String对象,这就极大地节省了空间也提高了运行效率。

6. 关于常量池的一些编程题(1)

  1. String s1 = "ab";
  2. String s2 = "abc";
  3. String s3 = s1 + "c";
  4. System.out.println(s3 == s2); //false 不相等,s1是变量,编译的时候确定不了值,在内存中会创建值,s3在堆内存中,。s2在常量池,所以不相等。
  5. System.out.println(s3.equals(s2)); //true 比较两个对象的值相等。

关于上述代码的解释:

String s1 = "abc"; String s2 = "abc";

s1会在常量池中创建,s2先查看常量池中有没有,如果有的话就指向它,如果没有就在常量池中创建一个然后指向它。所以s1和s2的两种比较是相同的。

7. 关于常量池的一些编程题(2)

  1. String s1 = new String("Hello");
  2. String s2 = new String("Hello");

答案是3个对象.

第一,行1 字符串池中的“hello”对象。

第二,行1,在堆内存中带有值“hello”的新字符串。

第三,行2,在堆内存中带有“hello”的新字符串。这里“hello”字符串池中的字符串被重用。

8. 浅谈一下String, StringBuffer,StringBuilder的区别?

  • String是不可变类,每当我们对String进行操作的时候,总是会创建新的字符串。操作String很耗资源,所以Java提供了两个工具类来操作String :StringBuffer和StringBuilder。
  • StringBuffer和StringBuilder是可变类,StringBuffer是线程安全的,StringBuilder则不是线程安全的。所以在多线程对同一个字符串操作的时候,我们应该选择用StringBuffer。由于不需要处理多线程的情况,StringBuilder的效率比StringBuffer高。
  • 引申问题:StringBuffer为啥是线程安全的? —StringBuffer里所有的方法都被synchronized 修饰:

参考|引用

https://blog.csdn.net/justloveyou_/article/details/52464440

https://www.jianshu.com/p/875a3d2b5690

https://www.jianshu.com/p/9c7f5daac283

Java面试炼金系列 (1) | 关于String类的常见面试题剖析的更多相关文章

  1. Java中String类的常见面试题

    1. 判断定义为String类型的s1和s2是否相等 String s1 = "ab"; String s2 = "abc"; String s3 = s1 + ...

  2. String类的常见面试题(3)

    1.判断定义为String类型的s1和s2是否相等 String s1 = "abc"; //这个"abc"对象首先会进常量池 String s2 = &quo ...

  3. Java面试指北!13个认证授权常见面试题/知识点总结!| JavaGuide

    大家好,我是 Guide哥!端午已过,又要开始工作学习啦! 我发现有很多小伙伴对认证授权方面的知识不是特别了解,搞不清 Session 认证.JWT 以及 Cookie 这些概念. 所以,根据我根据日 ...

  4. 大宇java面试系列(三):Redis常见面试题

    1. Redis 是什么?都有哪些使用场景? 我们先来理解经典的CAP理论: 一致性:是指从数据层面来看的一致性. 可用性:是指从系统层面的可用性. 容错性:是指从网络层面的的容错性. 数据库逐渐从关 ...

  5. String字符串相加常见面试题

    String name1="jack"; String name2="jack"; System.out.println(name1==name2); // t ...

  6. Java基础扫盲系列(-)—— String中的format

    Java基础扫盲系列(-)-- String中的format 以前大学学习C语言时,有函数printf,能够按照格式打印输出的内容.但是工作后使用Java,也没有遇到过格式打印的需求,今天遇到项目代码 ...

  7. Java第二次作业——数组和String类

    Java第二次作业--数组和String类 学习总结 1.学习使用Eclipse关联jdk源代码,查看String类的equals()方法,截图,并学习其实现方法.举例说明equals方法和==的区别 ...

  8. 夯实Java基础系列16:一文读懂Java IO流和常见面试题

    本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...

  9. 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!

    本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...

随机推荐

  1. 面试中的老大难-mysql事务和锁,一次性讲清楚!

    众所周知,事务和锁是mysql中非常重要功能,同时也是面试的重点和难点.本文会详细介绍事务和锁的相关概念及其实现原理,相信大家看完之后,一定会对事务和锁有更加深入的理解. 本文主要内容是根据掘金小册& ...

  2. 利用Express+MySQL进行简单的增删改查

    前言: 随着JavaScript语言的快速发展,其功能越来越强大,能做的事情也越来越多. 目前,web前端工程师能够利用NodeJS搭建服务,也成为了越来越多互联网公司对前端开发的硬性要求. 本文主要 ...

  3. POJ2806 Square

    题目描述 给定\(2*1\)和\(2 * 2\)两种规格的地砖,请问\(2 * n\)的地面总共有多少种方法? 下面是铺满\(2*17\)的地面的示意图. 输入输出格式 输入 多组数据,每组数据包括1 ...

  4. Android 本地缓存Acache的简单使用

    设置缓存数据: ACache mCache = ACache.get(this); mCache.put("key1", "value"); //保存6秒,如果 ...

  5. SpringSecurity权限管理系统实战—四、整合SpringSecurity(上)

    目录 SpringSecurity权限管理系统实战-一.项目简介和开发环境准备 SpringSecurity权限管理系统实战-二.日志.接口文档等实现 SpringSecurity权限管理系统实战-三 ...

  6. Mybatis 中判断参数长度

    <if test="params.length()!=2">

  7. Vue3.0数据响应式原理

    在vue2版本中响应式使用的是ES5对象的操作,通过遍历对象Object.defineProperty属性值的变化,实现监听数据 在3.0中使用的ES6版本的Proxy代理对象方式来实现数据的监听,省 ...

  8. TCP协议中的三次握手和四次挥手(图解)-转

    转自:http://blog.csdn.net/whuslei/article/details/6667471/ 建立TCP需要三次握手才能建立,而断开连接则需要四次握手.整个过程如下图所示: 先来看 ...

  9. Shell编程—正则表达式

    1什么是正则表达式 1.1定义 正则表达式是你所定义的模式模板,Linux工具可以用它来过滤文本.Linux 工具(比如sed编辑器或gawk程序)能够在处理数据时使用正则表达式对数据进行模式匹配. ...

  10. 使用Java8中的Optional类来消除代码中的null检查

    简介 Optional类是Java 8新增的一个类,Optional 类主要解决的问题是臭名昭著的空指针异常(NullPointerException). —— 每个 Java 程序员都非常了解的异常 ...