在之前的文章中,已经发布了常见的面试题,这里我了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。
本文主要描述字符串的知识点,下面先看下面一段代码:
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = "abc";
String s3 = new String("abc");
String s4 = "abc";
String s5 = "a"+"bc";
String s6 = new String("a")+"bc";
System.out.println(s1 == s2);//false
System.out.println(s1.equals(s2));//true
System.out.println(s1 == s3);//false
System.out.println(s2 == s3);//false
System.out.println(s2 == s4);//true
System.out.println(s2 == s5);//true
System.out.println(s2 == s6);//false
System.out.println(s1 == s1.intern());//false
System.out.println(s2 == s1.intern());//true
System.out.println(s2 == s2.intern());//true
System.out.println(s1.intern() == s3.intern());//true
}
上面的题目涉及到的知识点有equals和 == 的区别,jvm的内存模型,intern方法的使用,字符串String常量类。那我们就先把以上涉及的知识点描述一遍之后再来分析答案。
Java中== 和equals的区别是什么?
在实际项目中,我们常常会碰到判断两个相等的情况。在java语言中,== 和equals 都是有判断两者相等的意思,不过,使用场景上有些不同。其主要区别如下:
== 是运算符,equals是Object的基本方法,也就是说每个java类都有这个方法。在java语言中,运算符是不允许重载的,而方法是可以重写的,所以我们可以定义自己的equals的方法,这就意味着equals方法在每个类中有可能判断相等的方式不一样。
== 运算符判断相等的方法是:如果是基本类型的话,则判断的是基本类型的值是否相等;如果是引用类型的话,则判断引用的地址是否相等。而equals方法,我们知道如果不重写的话,那么其底层的实现其实就是 == (参考object的)。
//object 类
public boolean equals(Object obj) {
return (this == obj);
}
但是如果重写之后的话,我们就要根据类的重写去看下具体的实现了,在这里,我们看下Integer和String的判断思路。
//string
public boolean equals(Object anObject) {
if (this == anObject) {//先判断是否相等
return true;
}
if (anObject instanceof String) {//依次对比每个字符
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
//Integer
public boolean equals(Object obj) {
//判断是否是整型
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();//使用==判断
}
return false;
}
从上面的源码中可以看到,实际上java需要在object中添加equals方法,主要是因为==这个无法重写,那如果我们想要判断是否相等的话,可能就么办法了,所以java干脆写个方法,方便大家自己实现。
jvm的内存模型
Jvm的内存模型是后续jvm中的重点内容,由于刚刚的题目涉及到这块的知识,在这里,先简单介绍下jvm (1.7版本)分为:方法区、堆、栈、本地方法栈、程序计数器。而到了1.8版本,稍微做了些调整,使用了元数据区替换了方法区,并且元数据区并不在jvm的控制范围内,其使用的是本地内存,那么jvm中就只有堆、栈、本地方法栈、程序计数器。在java中,为了避免字符串常量分配的性能消耗,java在初始化字符串的时候,会为字符串放置到字符串常量池中。而字符串常量池在不同的版本实际上存放的位置也是不同的,在1.6版本时放在方法区,在1.7版本又移到了堆中,而到了1.8版本被移动到元空间。Jvm的知识后续还会重点介绍,在这里,主要是说明字符串在jvm中存储位置,方便理解上面的题目。
字符串的intern方法
该方法的作用是:如果某个字符串没有存入常量池,那现在常量池中创建,然后将常量池中的字符串返回。如果已经存在,那就直接将该字符串返回。
该方法的作用主要在于:1)节省内存,如果常量中存在的话,那就只有一个对象了。 2)如果大家都用intern的话,可以直接使用==来进行判断是否相同,而不需要使用equals,因为equals的效率是低下的。
字符串是一定不能修改的么?
对于这个问题,我们知道String这个类使用的是final char数组保存数据,如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];//指向的地址不能改变
}
该数组的地址使用final修饰,那么意味着地址实际上是不能修改的,但是并不意味着里面的值是不能修改的,所以,如果我们能够获取到value的值,是能够将其修改的。但是现在value是private修饰的,并且没有提供获取value的方法,再者String是一个final类,也无法继承。那是不是就无法获取到真正的值了。显然不是的,如果我们使用反射的话,是可以无视private属性的,并且获取到value的值,如果我们将获取到的value的值进行修改,那也就意味着我们能够修改相应的值。
Ok,有了这些基本的知识之后,我们可以对上面的代码进行解答了。回到代码本身,我们依次画出每个存入的位置,知道位置之后,就知道==是否相等了。
上面是画出的每个对象指向的地址,这里常量池不指定是在哪,因为上面介绍了不同版本存入的位置也不相同。在这里有几个需要说明的是:
- 使用intern返回一定是字符串常量池的位置
- 使用new出来的字符串,一定会在堆中存在字符串,并且指向的是堆中的位置
- +运算符对于字符串来说,需要注意的是,在初始化赋值的时候,如果常量字符串+常量字符串(s5 = “a”+”bc”),那么返回的是存入常量池中计算结果(“abc”)。如果是存在其他的需要找地址计算值的话(s6 = new String(“a”) +”bc”),那么只会返回存在堆的计算结果,并不会将结果存入到常量池中。
知道上面的知识点之后,对解决上面的题目应该是ok的。
字符串除了上面的知识点之外,我们还有几个相关的知识点也在本文中说明。
说明一下StringBuilder、StringBuffer和String的区别?
这个是面试可能会出现的问题,虽然现在已经比较少使用StringBuilder或者StringBuffer了,但是我们还是应该要知道一下这两个类出现的作用是什么?
其实核心的问题在于String是不可变,那字符串的拼接就变得不是那么友好,String在做字符串拼接的时候,需要重新申请内存存入新的字符串。基于这样的一个因素,StringBuilder和StringBuffer出现了,其主要的作用是在字符串拼接的效率上。一般来说字符串的拼接速度是StringBuilder>StringBuffer>String。而StringBuilder和StingBuffer的区别在于StringBuilder是线程不安全的,StringBuffer是线程安全的。下面我们看下其append方法,底层都是基于父类实现的:
public AbstractStringBuilder append(String str) {//都是基于父类实现的
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
而StringBuilder和StringBuffer的区别在于:方法上是否加锁
//StringBuilder
public StringBuilder append(String str) {
super.append(str);
return this;
}
//StringBuffer
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
Java中hashCode 和equals方法的区别?
我们知道hashCode 和equals方法都是父类Object中的基本方法。HashCode 方法定义的初衷是为了求解一个类的hashCode值,而equals方法的定义初衷是为了判断两个对象是否内容一致。一般情况下,我们会认为equals相等的两个对象,其hashcode值也相等。而hashcode相等的两个对象,equals不一定相等。在《effect java》中有一条重要的规则是:如果重写了equals方法,就一定要重写hashcode方法。规则很简单,原因在于如果我们只重写了其中的一个,可能会导致在某些java的基本类中判断两个对象是否相等的时候出错,尤其是在hashMap中存入的对象作为key的时候,该对象一定要重写两个方法。因为hashMap在判断key是否相同的时候,是先判断hashcode后再判断equals的。
本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞。谢谢。
想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈