2026Java常见面试题汇总
String、StringBuilder、StringBuffer的区别
String是不可变字符串,而StringBuilder和StringBuffer是可变字符串。
String字符串对象是不可变对象,每次操作都会返回新的字符串,原有字符串不会被改变;StringBuilder和StringBuffer是可变字符串,他们的字符串对象可以被更改,对字符串的操作不会生成新的对象,即对同一个字符串进行修改。
StringBuilder 是线程不安全的,适用于单线程环境;而StringBuffer是线程安全的,适用于多线程环境。它们两个都适用于大量字符串拼接的场景,因为它们对于字符串的拼接操作来说,不仅效率高,而且不占用额外的空间。
什么方法能改变String
// 可以通过反射来改变String的值 String name = "xiaoao"; Field field = String.class.getDeclaredField("value"); field.setAccessible(true); char[] values = (char[]) field.get(name); System.out.println(name + " " + name.hashCode()); values[0] = 'z'; System.out.println(name + " " + name.hashCode()); // 输出: // xiaoao -759499955 // ziaoao -759499955运行项目并下载源码java运行
虽然String的字符数组声明为final,并且被private修饰,但是这个final仅仅只是让value的引用不改变,而不能让字符数组的字符不能改变。可以看到上面的代码,即时字符串的值发生了改变,但是它的hashCode仍然是一样的。
private int hash; // Default to 0 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; }运行项目并下载源码java运行
为什么hashCode不会改变呢?因为在第一次调用过hashCode之后,字符串对象内通过hash这个属性缓存了hashCode的计算值,只要缓存过之后就不会再计算了,所以hashCode的值不会发生改变。
StringBuilder和StringBuffer的线程安全问题
场景:在方法的内部,一个String类型的List,将这些元素用逗号分割拼接成一个完整的字符串,然后作为方法的返回值返回,这个方法可能在多线程的环境下,然后用StringBuilder或者StringBuffer会产生线程安全问题吗
(1)使用StringBuilder在多线程情况下拼接字符串,会产生线程安全问题;而StringBuffer则不会。
(2)为什么StringBuilder会产生线程安全问题?因为StringBuilder底层的append方法调用的其实是父类的append方法。而AbstractStringBuilder的append方法中有一个字符串长度的相加的操作count += len,这个操作不是一个原子操作:
- 首先从内存中加载count到寄存器
- 在寄存器执行相加操作
- 将结果写入内存
在多线程环境下,各个线程的操作的结果可能被其他线程给覆盖掉,所以,得到的结果不会是正确结果。
而且还有可能会抛出下标越界异常,这是因为StringBuilder会进行扩容操作,如果在进行扩容操作的时候,数组的长度被其他线程修改了,那么可能扩容后的数组放不下新的字符串,就会出现数组下标越界异常。
String有长度限制吗
String类型的对象是有长度限制的,String对象并不能“存储”无限制长度的字符串。关于String的长度限制要从编译时限制和运行时限制两方面考虑。
(1)编译时限制。
对于String s = "abc",这样的字符串声明,字符串常量abc肯定会被放入方法区的常量池中,String长度之所以会受到限制,是因为JVM的规范对常量池有所限制。常量池中的每一种数据项都有自己的类型。Java中UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8类型表示。
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }运行项目并下载源码java运行
bytes数组就是真正存储常量数据的地方。而length就是数组可以存储的最大字节数。length的类型是u2,u2是无符号的16位整数,因此理论上允许的最大长度是216-1 = 65535。所以上面byte数组的最大长度是65535。
//65535个d,编译报错 => java: 常量字符串过长 String s = "dd..dd"; //65534个d,编译通过 String s1 = "dd..d";运行项目并下载源码java运行
起始,Javac编译器的额外限制,在Javac源代码中可以看到:
private void checkStringConstant(DiagnosticPosition var1, Object var2) { if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) { this.log.error(var1, "limit.string", new Object[0]); ++this.nerrs; } }运行项目并下载源码java运行
代码中可以看出,当参数类型为String,并且长度大于等于65535的时候,就会导致编译失败。
注意:String的限制并不是对字符串长度的限制,而是对字符串底层存储的限制。Java中的字符串都是使用UTF8编码的,UTF8编码使用1~4个字节来表示具体的Unicode字符。所以有的字符占1个字节,而平常使用的中文需要3个字节来存储。
//65534个a,编译通过 String s1 = "aa..a"; //21845个中文”自“,编译通过 String s2 = "好...好"; // 一个英文字母a加上21845个中文”自“,编译失败 => java: 对于常量池来说, 字符串 "a好好好好好好好好好好好好好好好好好好好..." 的 UTF8 表示过长 String s3 = "a好...好";运行项目并下载源码java运行
对于s1,一个字母a的UTF8编码占用一个字节,65534字母占用65534个字节,长度是65534,也没超过Javac的限制,所以可以编译通过。
对于s2,一个中文占用3个字节,21845个正好占用65535个字节,而且字符串长度是21845,并没有超过javac对长度的限制,所以可以编译通过。
对于s3,一个英文字母a加上21845个中文”自“占用65535个字节,超过了最常限制,编译失败。
(2)运行时限制
String运行时的限制主要体现在String的构造函数上。
// 分配一个新的String,offset参数是子数组的第一个字符的索引,count参数指定子数组的长度 public String(char value[], int offset, int count) { ... }运行项目并下载源码java运行
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、场景题、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
上面的count值也就是字符串的最大长度。在Java中,int的最大长度是231-1。所以在运行时,String的最大长度就是231-1。
但是这个也是理论上的长度,实际长度还是要看JVM的内存。一个最大的字符串会占用(2^31-1)*2*16/8/1024/1024/1024 = 4GB,所以在最坏的情况下,一个最大的字符串需要4GB的内存,如果虚拟机不能分配这么多内存,则直接报错。
(2^31-1)*2*16/8/1024/1024/1024 = 4GB 在 Unicode 编码下,每个字符通常占用 2 个字节(16 位),即使用 2 个字节表示一个字符。(char占2个字节) 由于 1 GB = 1024 MB = 1024 KB = 1024 Bytes,所以可将该字节数转换为单位 GB运行项目并下载源码
- 1
- 2
- 3
(3)总结:
String的长度是有限制的:
- 编译期限制:字符串的UTF8编码值的字节数不能超过65535,字符串的长度不能超过65534。
- 运行时限制:字符串的长度不能超过231 - 1,占用的内存数不能超过虚拟机能提供的最大值。
泛型
什么是泛型
Java泛型是JDK5中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。
**编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。**比如List<Person> persons = new ArrayList<>();这行代码就指明了ArrayList对象只能传入Person对象,如果传入其他类型的对象就会报错。
使用泛型的意义
(1)适用于多种数据类型执行相同的代码,即代码复用。
(2)泛型中的类型在使用时指定,不需要进行强制类型转换,即类型安全,编译器会检查类型。
如何使用泛型
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。
<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。运行项目并下载源码java运行
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
如何理解Java中的泛型是伪泛型
Java泛型这个特性是从JDK1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
泛型的类型擦除的原则是:
- 消除类型参数声明,即删除
<>及其包围的部分。 - 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
List如果不用泛型会出现什么问题
如果List不使用泛型,那么默认的数据类型是Object。
(1)需要强制类型转换。在想要获取对应的元素的类型时,必须要进行强制转换。
(2)可读性较差,很可能会出现ClassCastException。在使用的时候,如果不考虑兼容,那么会出现类型转换异常。
泛型的好处
**(1)类型的安全。**泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在很高的程度上验证类型假设。
**(2)消除强制类型转换。**泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
**(3)潜在的性能收益。**泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
反射
什么是反射
反射是在运行状态中
对于任意一个类,都能知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取信息以及动态调用对象的方法功能称为Java语言的反射机制。
反射机制的应用场景
(1)JDBC中,利用反射动态加载了数据库驱动程序。
(2)Web服务器中利用反射调用了Servlet的服务方法。
(3)很多框架都用到反射机制,注入属性,调用方法,比如spring,使用@Component注解就可以声明一个类为SpringBean,这就是基于反射获取类,然后再获取类上的注解,进行一定的逻辑梳理。
除此之外,除了使用new创建对象之外,还可以使用Java反射创建对象。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、场景题、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
反射机制的优缺点
(1)优点:
- 能够在运行时动态获取类的实例,提高灵活性
- 可以和动态编译相结合
(2)缺点:
- 对性能有影响,需要解析字节码,将内存中的对象进行解析,这类操作总是慢于直接执行Java代码
(3)解决方法:
- 通过
setAccessible(true)关闭JDK的安全检查来提升反射速度 - 多次创建一个类的实例时,有缓存会快很多
- 可以使用ReflectASM工具类,通过字节码生成的方式加快反射速度
注解
什么是注解
Annotation(注解)是JDK5开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质上就是继承了Antation接口。
注解解析的方法
注解只有被解析后才会生效,常见的解析方法有两种:
编译期直接扫描:编译器在编译Java代码的时候扫描对应的注解并处理,比如某个方法使用@Override注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的@Value、@Component)都是通过反射来进行处理的。
