String
1、String 是 Java 基本数据类型吗?可以被继承吗?为什么?
String是Java基本数据类型吗?
不是。Java 中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type)。
String是一个比较特殊的引用数据类型。
String 类可以继承吗?
不行。String 类使用 final 修饰,是所谓的不可变类,无法被继承。
String 类为什么要被设计成不可继承?
- 字符串是一种非常重要且常用的数据类型,它在 Java 程序中被广泛使用。为了保证字符串类型的安全性和可靠性,Java 的设计者决定不允许用户扩展它。
- 同时,由于字符串是一种常量类型,一旦创建就不能更改。所以,即使你能够继承 String 类并扩展它的功能,也没有太多的意义。
2、String,StringBuffer,StringBuilder 的区别是什么?
可变和不可变:
- String是final修饰符修饰的字符数组,所以是不可变的,如果操作的是少量的数据,则可以使用String;
- StringBuilder和StringBuffer是可变的字符串数组;
是否多线程安全:
- String中的对象是不可变的,也就可以理解为常量,显然线程安全。
- StringBuilder是非线程安全的,因为Stringbuilder继承了父类abstractStringBuilder的append方法,该方法中有一个count+=len的操作不是原子操作,所以在多线程中采用StringBuilder会丢失数据的准确性并且会抛ArrayIndexOutOfBoundsException的异常。
- StringBuffer是线程安全的,因为他的append方法被synchronized关键字修饰了,所以它能够保证线程同步和数据的准确性 。
性能:
- 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象,效率低。
- 因为StringBuffer是被synchronized修饰的,所以在单线程的情况下StringBuilder的执行效率是要比StringBuffer高的,所以一般在单线程下执行大量的数据使用StringBuilder,多线程的情况下则使用StringBuffer。
3、String str1 = new String("abc")和String str2 = "abc" 的区别?
两个语句都会去字符串常量池中检查是否已经存在 “abc”,如果有则直接使用,如果没有则会在常量池中创建 “abc” 对象。
堆与常量池中的String
但是不同的是,String str1 = new String("abc") 还会通过 new String() 在堆里创建一个 "abc" 字符串对象实例。所以后者可以理解为被前者包含。
String s = new String("abc")创建了几个对象?
很明显,一个或两个。如果字符串常量池已经有“abc”,则是一个;否则,两个。
当字符创常量池没有 “abc”,此时会创建如下两个对象:
- 一个是字符串字面量 "abc" 所对应的、字符串常量池中的实例
- 另一个是通过 new String() 创建并初始化的,内容与"abc"相同的实例,在堆中。
4、String有哪些特性?
- 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性;
- 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用;
- final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
5、在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
6、String为什么要设计成不可变的?
- 便于实现字符串池
- 在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
- 如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
- 使多线程安全
- 在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
- 避免安全问题
- 在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
- 加快字符串处理速度
- 由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
7、两个字符串相加的底层是如何实现的?
- 如果拼接的都是字符串常量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。
- 如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()方法,将这些字符串拼接在一起。
8、intern方法有什么作用?
JDK源码里已经对这个方法进行了说明:
* <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p>
意义:
- 如果当前字符串内容存在于字符串常量池(即equals()方法为true,也就是内容一样),直接返回字符串常量池中的字符串
- 否则,将此String对象添加到池中,并返回String对象的引用。
9、字符串拼接的方式有哪些?
- 直接用
+
,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用+
拼接,相当于不断创建新的StringBuilder
对象再转换成 String 对象,效率极差。 - 使用 String 的 concat 方法,该方法中使用
Arrays.copyOf
创建一个新的字符数组buf
并将当前字符串value
数组的值拷贝到buf
中,buf
长度 = 当前字符串长度 + 拼接字符串长度。之后调用getChars
方法使用System.arraycopy
将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用+
。 - 使用
StringBuilder
或StringBuffer
,两者的append
方法都继承自 AbstractStringBuilder,该方法首先使用Arrays.copyOf
确定新的字符数组容量,再调用getChars
方法使用System.arraycopy
将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer
使用synchronized
保证线程安全。
10、String.hashCode()源码了解多少?
String.hashCode()源码:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
从源码中我们可以看出:
- String有一个私有变量hash来缓存哈希值,即当该串第一次调用hashCode()方法时,hash默认值为0,继续执行,当字符串长度大于0时计算出一个哈希值赋给hash,之后再调用hashCode()方法时不会重新计算,直接返回hash;
- 计算时,使用的是该字符串截成的一个字符数组,用每个字符的ASCII值进行计算,根据注释可以看出哈希计算公式是:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],n是字符数组的长度,s是字符数组;
- 算法中还有一个乘数31,为什么使用31呢?
- hash函数必须选用质数,这是被科学家论证过的hash函数减少冲突的理论;
- 如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为使用偶数相当于位移运算(低位补0);
- 31 * i 可以用 (i << 5) - i 来计算,而移位操作的效率高于乘法,所以这是基于性能角度的考虑;
- 31是个不大不小的质数,兼顾了性能和冲突率,太小hash冲突概率大,太大过于分散占用存储空间大,所以选择一个不大不小的质数很有必要。
11、String类的equals()源码了解多少?
String类的equals()源码:
public boolean equals(Object anObject) { // 检查两个字符串是否指向同一个对象。如果是,则直接返回 true if (this == anObject) { return true; } // 检查给定的对象是否是 String 类型的。如果不是,则返回 false if (anObject instanceof String) { String anotherString = (String) anObject; int n = count; // 比较两个字符串的长度是否相等。如果不相等,则返回 false if (n == anotherString.count) { char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; // 逐个比较两个字符串的每个字符是否相等。如果有任意一个字符不相等,则返回 false; while (n-- != 0) { if (v1[i++] != v2[j++]) return false; } // 否则,返回 true return true; } } return false; }
12、String源码中有哪些地方被final修饰?
java.lang.String 类的源码中,有以下几处地方被 final 修饰:
- private final char[] value:表示字符串的值的数组。这个字段被 final 修饰,意味着一旦初始化完成就不能再更改。
- private final int offset:表示字符串的值数组的偏移量。这个字段被 final 修饰,意味着一旦初始化完成就不能再更改。
- private final int count:表示字符串的值数组的长度。这个字段被 final 修饰,意味着一旦初始化完成就不能再更改。
- private final int hash:表示字符串的哈希值。这个字段被 final 修饰,意味着一旦初始化完成就不能再更改
- public final class String:这个类被 final 修饰,意味着其不可以被继承。
- 此外,在 java.lang.String 类中还有若干个方法被 final 修饰,这些方法不能被子类覆盖。例如,final int length() 方法就是一个返回字符串长度的方法,它被 final 修饰,意味着不能被子类覆盖。
java.lang.String 类的源码如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // 字符串的底层实现是一个 private 修饰的字符数组 private final char value[]; // 构造函数,用于创建一个新的字符串对象 public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } // 构造函数,用于创建一个新的字符串对象 public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); } // 构造函数,用于创建一个新的字符串对象 public String(String original) { this.value = original.value; } // 返回字符串对象的长度 public int length() { return value.length; } // 返回指定位置的字符 public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; } // 返回一个新的字符串对象,它是原始字符串的子串 public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, endIndex - beginIndex); } // 返回一个新的字符串对象,它是原始字符串的副本 public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } char buf[] = Arrays.copyOf(value, value.length + otherLen); str.getChars(buf, value.length); return new String(buf, true); } // 返回一个新的字符串对象,它是原始字符串的大写形式 public String toUpperCase(Locale locale) { if (locale == null) { throw new NullPointerException(); } int len = value.length; for (int i = 0; i < len; ) { int c = Character.codePointAt(value, i); int srcCount = Character.charCount(c); if (c == Character.toUpperCase(c)) { i += srcCount; continue; } char[] tem = Character.toChars(Character.toUpperCase(c)); char[] buf = new char[len + tem.length - srcCount]; System.arraycopy(value, 0, buf, 0, i); int j = i; for (char c2 : tem) { buf[j++] = c2; } System.arraycopy(value, i + src