年纪越大越发现知识要系统性的学习,接下来我们聊下面试中经常被问到的String相关的问题。
一. String 是基本数据类型?占多少字节数?
String 是引用数据类型。
// 下面两种定义方法
String str = null;
String str = new String("hello");
不管你怎么定义,String都是引用数据类型。下面我们来列举下java的两种数据类型。
String 全称是 java.lang.String,是java的一个类,是引用数据类型。
通过查看String源码我们知道,String里面是一个char 数组。 而一般一个char字符默认占一个字节。
那么如果字符串里面有中文,具体占几个字节呢?
String str = "小叶檀";
char[] chars = str.toCharArray();
byte[] bytes = str.getBytes();
System.out.println("字符串转换成字符数组为:"+ Arrays.toString(chars));
System.out.println("字符串所占字节数:"+Arrays.toString(bytes));
// 运行结果为
字符串转换成字符数组为:[小, 叶, 檀]
字符串所占字节数:[-27, -80, -113, -27, -113, -74, -26, -86, -128]
从运行结果可以知道,中文是占两个字节。
二. 为什么说String是不可变的? 为什么会被设计成不可变? 真的不可变?
我们来看看java.lang.String的源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
}
- 为什么不可变?
String类 是一个被final修饰的类,里面是一个被final修饰的私有的字符数组。我们知道,被final修饰的类不能被继承,被final修饰的变量不能被重新赋值。另外,也没发现对于这个字符数组的setter()和getter()。所以说String是不可变的。
- 为什么会被设计成不可变?
看String源码我们发现有一个hash属性,上面注释说缓存的String的hashcode值。所以我们联想如果String是可变的,那么是不是每次改变都要重新计算hashcode,这样会很消耗性能。平时我们会使用String来存储数据库连接等关键变量,如果是可变的,那么是不是对于程序来说很不安全,万一被注入了呢?
所以String设计成不可变是在安全和性能上考虑的。
- 真的不可变?
不是的,我们可以通过反射机制来改变String的值。
public static void main(String[] args) {
String str = "hello";
System.out.println("更新前:"+str); // hello
try {
// 获取到String里面的私有属性value
Field field = String.class.getDeclaredField("value");
// 指定属性的访问权限为true
field.setAccessible(true);
// 修改值
char[] value = (char[])field.get(str);
value[1]='x';
System.out.println("更新后:"+str); // hxllo
}catch (NoSuchFieldException ex){
ex.printStackTrace();
}catch (IllegalAccessException e){
e.printStackTrace();
}
}
三. String的equals()和== 区别
equals() 比较对象里面的值,== 比较内存地址。
考虑到String的不可变,每次new就是新建一个对象放到堆里面,这样太耗内存、性能也不好。 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为 了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串常量池。
jdk1.6及以前字符串常量池放在方法区里面。jdk1.7 常量池被挪到堆里面,应该是考虑到这个池子会比较大,方法区太小。
String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2); // true
String s3 = new String("abc");
String s4 = new String("xyz");
System.out.println(s1==s3); // false
System.out.println(s1==s3.intern()); // true
当我们用双引号直接定义一个字符串时,jvm会先去检查字符串常量池中是否存在,如果不存在会在常量池里面创建一个String 对象,如果存在会直接引用。
intern() 是native方法,返回字符串对象的规范化表示形式,也就是把对象转化成字符串常量。可以联想下,当我们调用intern(),jvm会去字符串常量池查找或创建一个当前字符串对象的引用。
四. String a = new String("hello") 创建了几个对象?
存在两个对象。 当我们使用new的这种方式来创建一个字符串时,首先会在堆创建一个对象,然后去字符串常量池里面去查找,如果找到就引用,没有就在常量池里面创建一个。
五. String 是被哪个加载器加载的?自定义的String可以被加载?
java.lang.String会被顶级类加载器Bootstrap Classloader 加载。
自定义的String不会被加载。安装jvm的双亲委派原则,当加载到一个新类,加载器会默认让它的父类去加载,当父类返回无法加载时,AppClassLoader 才会加载。所以当加载到String时,AppClassLoader默认会把加载权让给其父类ExtClassLoader,
但是ExtClassLoader在jre/lib/ext目录下没有找到String.class类。接着又会把加载权让给其父类BootStrap ClassLoader,
BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。
六. String, Stringbuffer, StringBuilder 的区别
首先它们都是final类,不能被继承。
String 是字符串常量;
Stringbuffer 是 字符串缓冲池,读写都使用了synchronized来实现线程安全;
StringBuilder 效率最快,但是线程不安全。