《JAVA面经实录》- JAVA

一 、基础

1. 介绍下Java中的基本数据类型

基本类型

位数

大小(字节)

默认值

取值范围

封装类

byte

8

1

(byte)0

-128 ~ 127

Byte

short

16

2

(short)0

-32768(-2^15) ~ 32767(2^15 - 1)

Short

int

32

4

0

-2147483648 ~ 2147483647

Integer

long

64

8

0l

-9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1)

Long

float

32

4

0.0f

1.4E-45 ~ 3.4028235E38

Float

double

64

8

0.0d

4.9E-324 ~ 1.7976931348623157E308

Double

boolean

1

-

false

true、false

Boolean

char

16

2

\u0000(null)

0 ~ 65535(2^16 - 1)

Character

boolean: int 4个字

需要注意:

(1) int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。

(2) 基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。

(3)Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析。

(4)Java 里使用 float 类型的数据一定要在数值后面加上 f 或 F,否则将无法通过编译。

(5)char a = 'h' : 单引号,String a = "hello" : 双引号。

2.自增自减运算符

自增运算符:

无论这个变量是否参与到运算中去,只要用++运算符,这个变量本身就加1操作

只是说如果变量参与到运算中去的话,对运算结果是产生影响:

看++在前还是在后,如果++在后:先运算,后加1;如果++在前,先加1,后运算

自减运算符:

无论这个变量是否参与到运算中去,只要用--运算符,这个变量本身就减1操作

只是说如果变量参与到运算中去的话,对运算结果是产生影响:

看--在前还是在后,如果--在后:先运算,后减1; 如果--在前,先减1,后运算

3. Java有哪几种注释?

Java中的注释主要有三种形式,这三种注释形式在Java编程中都有其特定的用途和场景,合理使用注释可以提高代码的可读性和可维护性:

3.1 单行注释:

a. 使用双斜杠(//)开头,仅在该行有效。

b. 适用于对少量代码或特定代码行进行简单说明。

c. 示例:// 这是一个单行注释。

3.2 多行注释:

a. 使用/* 开始,以 */结束,可以跨越多行。

b. 适用于对一大段代码或复杂逻辑进行详细解释。

c. 示例:/* 这是一个多行注释,它可以跨越多行 */。

3.3 文档注释:

a. 使用/** 开始,以 */结束,通常出现在类、方法、字段等的声明前面。

b. 用于生成代码文档,如API文档,这种注释可以被工具(如Javadoc)提取并生成相应的文档。

c. 文档注释可以包含特定的标签。

4. Java中的关键字有哪些?

Java关键字一共有53个,其中包括48个关键字、2个保留字和3个特殊直接量。具体来说:

  • 48个关键字

访问修饰符publicprotectedprivate

类、接口、抽象类和实现接口、继承类、实例化对象:classinterfaceabstractimplementsextendsnew

包相关:importpackage

数据类型:bytecharbooleanshortintfloatlongdoublevoid

条件循环:ifelsewhileforswitchcasedefaultdobreakcontinuereturninstanceof

修饰方法、类、属性和变量:staticsuperfinalthisnativestrictfpsynchronizedtransientvolatile

错误处理:catchtryfinallythrowthrows

其他enumassert

  • 2个保留字

constgoto(这两个保留字在Java中目前没有实际用途,但可能在未来的Java版本中被用作关键字)

  • 3个特殊直接量

nulltruefalse

5. & 和 && 的区别?

&运算符是:逻辑与;&&运算符是:短路与。

1)&和&&在程序中最终的运算结果是完全一致的,只不过&&存在短路现象。如果是&运算符,那么不管左边的表达式是true还是false,右边表达式是一定会执行的。当&&运算符左边的表达式结果为false的时候,右边的表达式不执行,此时就发生了短路现象,也就是说&&会更加的智能。这就是他们俩的本质区别。

2)当然,&运算符还可以使用在二进制位运算上,例如按位与操作。

6. char 型变量中能不能存储一个中文汉字,为什么?

char 类型可以存储一个中文汉字,因为Java中使用的编码是Unicode编码,一个char 类型占2个字节(16 比特),所以放一个中文是没问题的。

补充:使用Unicode 意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是 Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如 InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务。

7. break ,continue ,return 的区别及作用

break 跳出总上一层循环,不再执行循环(结束当前的循环体) 。

continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) 。

return 程序返回,不再执行下面的代码(结束当前的方法 直接返回) 。

另外在switch...case语句中,break不是必须的,但是会较大影响到运行结果。如果没有break,后面的一旦有case匹配成功,后面的case将无条件的向下执行其它的case

8. switch 是否能作用在 byte 上,能否作用在 long 上,能否作用在 String 上?

早期的 JDK 中,switch(expr)中,expr 可以是 byte、short、char、int。从 1.5 版开始,Java 中引入了枚举类型(enum),

expr 也可以是枚举,从 JDK 1.7 版开始,还可以是字符串(String)。长整型(long)是不可以的。

9. Math.round(11.5)等于多少?Math.round(- 11.5) 又等于多少?

Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行取整。

10. short s1 = 1; s1 = s1 + 1; 有错吗?short s1 = 1; s1 += 1 有错吗?

前者不正确,后者正确。

对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。

而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。

11.数组有没有length()方法?String有没有length()方法?

Java中的数组没有length()方法,但是有length属性。String有length()方法。

12. 用最有效率的方法计算2乘以8?

2 << 3,将2左移3位

案例: 4乘以8最快的方式: 4<<3

  • << 左移

3<<2 = 12

面试题: 4乘以8最快的方式: 4<<3

  • >> 有符号右移

6>>2 = 1

  • >>> 无符号右移

6>>>2 = 1

13. 下面 Integer 类型的数值比较输出的结果为?

public class Test{
    public static void main(String[] args) {
       Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150; System.out.println(f1 == f2);
       System.out.println(f3 == f4);
    }
}

f1==f2的结果是 true,而f3==f4 的结果是false。为什么呢?先来说说装箱的本质。当我们给一个Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf,如果看看valueOf的源代码就知道发生了什么。如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer 对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2的结果是 true,而f3==f4 的结果是false。

14. int 和 Integer 有什么区别?

Java 的JDK从 1.5 开始引入了自动装箱/拆箱机制。它为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是 Integer,其它基本类型对应的包装类如下:原始类型: boolean,char,byte,short,int,long,float,double包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double。

15. 什么是方法的返回值?返回值的作用是什么?

方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能 产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作!

16. 如何将数值型字符转换为数字?

调用数值类型相应包装类中的方法 parse***(String)或 valueOf(String) 即可返回相应基本类型或包装类型数值;

17. 如何将数字转换为字符?

将数字与空字符串相加即可获得其所对应的字符串;另外对于基本类型 数字还可调用 String 类中的 valueOf(…)方法返回相应字符串,而对于包装类型数字则可调用其 toString()方法获得相应字符串;

18. 如何取小数点前两位并四舍五入?

可用该数字构造一 java.math.BigDecimal 对象,再利用其 round()方法 进行四舍五入到保留小数点后两位,再将其转换为字符串截取最后两位。

19. 3*0.1 == 0.3 将会返回什么? true 还是 false?

false,因为有些浮点数不能完全精确的表示出来。

20 . Java移位运算符?

java中有三种移位运算符 

<< :左移运算符,x << 1,相当于x乘以2(不溢出的情况下),低位补0 

>> :带符号右移,x >> 1,相当于x除以2,正数高位补0,负数高位补1 

>>> :无符号右移,忽略符号位,空位都以0补齐

21. 基本类型和包装类型的区别?

Java中的基本类型(Primitive Types)和包装类型(Wrapper Classes)之间存在多个关键区别,这些区别主要体现在以下方面:

一、包含内容与性质

  • 基本类型:只包含数据本身,不包含任何方法或操作。它们不是对象,因此没有对象的特性,如继承和多态。
  • 包装类型:不仅包含数据,还包含了一系列的方法(如类型转换、比较等)和属性,是对基本类型数据的封装。包装类型是对象,具有对象的所有特性,如继承自Object类的功能(如toString()、equals()等)。

二、声明方式与存储位置

  • 基本类型:直接声明变量并赋值,不需要使用new关键字。它们直接将值保存在栈内存中,访问速度较快。
  • 包装类型:需要使用new关键字在堆内存中分配内存空间,或者使用自动装箱(JDK 5及以上版本支持)来创建对象。包装类型将对象放在堆内存中,通过栈中的引用来调用它们,访问速度相对较慢,并且需要考虑垃圾回收等额外开销。

三、初始值

  • 基本类型:在声明时如果没有显式赋值,则会被赋予一个默认值。例如,int的默认值为0,boolean的默认值为false。
  • 包装类型:在声明时如果没有显式赋值,则默认值为null,因为它们是引用类型。

四、使用方式

  • 基本类型:直接用于数值计算、位运算等场景,效率较高。但由于它们不是对象,因此不能在需要对象的场合(如集合中)直接使用。
  • 包装类型:主要用于需要对象的地方,如集合(List、Map等)中只能存储对象,因此基本类型需要通过包装类来转换为对象才能存储在集合中。此外,包装类型还提供了丰富的操作方法和常量。

五、泛型适用性

  • 基本类型:不能直接用于泛型,因为泛型在编译时会进行类型擦除,而基本类型没有对应的类型信息可以擦除。
  • 包装类型:可以用作泛型的类型参数,因为它们是对象类型,具有类型信息。例如,可以使用List<Integer>来存储整数对象。

六、内存占用与性能

  • 基本类型:通常占用较少的内存空间,因为它们只存储数据本身。
  • 包装类型:由于它们是对象,因此需要额外的内存来存储对象头和引用等信息。这可能导致在大量使用包装类型时增加内存开销。此外,自动装箱和拆箱操作也会消耗一定的性能。

综上所述,Java中的基本类型和包装类型在包含内容、性质、声明方式、存储位置、初始值、使用方式、泛型适用性、内存占用与性能等方面都存在明显的区别。在开发中,应根据具体需求选择合适的类型以提高代码的可读性、可维护性和性能。

22. 包装类型的缓存机制了解么?

Java包装类型的缓存机制是Java中一个重要的性能优化手段。以下是对Java包装类型缓存机制的详细解释:

一、缓存机制概述

Java中的包装类(Wrapper Class)是为了将基本数据类型转换为对象而存在的。包装类都位于java.lang包中,使用时无需显式导入。包装类型缓存机制指的是,在某些情况下,Java会对一定范围内的包装类对象进行缓存,以减少对象的创建和销毁,从而提高性能和节省内存空间。

二、缓存机制的实现

包装类型的缓存机制是通过静态成员变量来实现的。在Integer、Long、Short、Byte、Character这五个包装类中,定义了一个静态数组cache[],用于缓存常用的数值对象。当使用valueOf()方法创建包装类对象时,会先检查该值是否在缓存范围内。如果是,则直接返回缓存中的对象;否则,创建一个新的对象并可能放入缓存中(注意,对于超出缓存范围的新对象,有的包装类并不会将其放入缓存,这取决于具体的实现)。

三、各包装类的缓存范围

  1. Integer:默认缓存了-128到127之间的整数。这个范围是根据实际应用中整型数据的常用范围来设定的,能够覆盖大多数常用情况。
  2. Long:默认缓存了-128到127之间的长整数。
  3. Short:默认缓存了-128到127之间的短整数。
  4. Byte:默认缓存了-128到127之间的字节。由于byte的值范围本身就是-128到127,所以所有的Byte对象都使用缓存。
  5. Character:默认缓存了0到127之间的字符。这是因为ASCII字符集只定义了128个字符,而Unicode字符集的前128个字符与ASCII字符集完全相同。
  6. Boolean:只缓存了true和false两个对象。

需要注意的是,浮点数类型的包装类(Float和Double)并没有实现缓存机制,主要是因为浮点数的表示范围非常大,且使用场景多样,缓存效果并不明显。

23. 自动装箱与拆箱了解吗?原理是什么?

Java中的自动装箱与拆箱是Java 5引入的一项特性,它允许Java编译器在需要时自动地将基本数据类型转换为对应的包装类类型(自动装箱),或者将包装类类型转换为对应的基本数据类型(自动拆箱)。这一特性极大地简化了代码编写,提高了代码的可读性和可维护性。

1. 自动装箱的原理:

当需要将一个基本数据类型(如int、char等)赋值给一个对应的包装类类型(如Integer、Character等)的变量时,Java编译器会自动调用该包装类的valueOf()方法,将基本数据类型转换为包装类对象。例如,Integer i = 100; 这行代码实际上会被编译器转换为 Integer i = Integer.valueOf(100);。

2. 自动拆箱的原理:

当需要将一个包装类类型的变量赋值给一个基本数据类型(或其对应的包装类类型的变量参与基本数据类型的运算或比较)时,Java编译器会自动调用该包装类的xxxValue()方法(如intValue()、charValue()等),将包装类对象转换为对应的基本数据类型。例如,int j = i; 这行代码(假设i是一个Integer类型的变量)实际上会被编译器转换为 int j = i.intValue();

24. 超过 long 整型的数据应该如何表示?

  1. 使用BigInteger类:BigIntegerjava.math包中的一个类,它提供了任意精度的整数运算。你可以使用BigInteger来表示和操作非常大的整数。
  2. 使用BigDecimal类(如果涉及小数):如果你需要表示非常大的小数,可以使用BigDecimal类。它也是java.math包中的一个类,提供了任意精度的浮点数(实际上是定点数)运算。

二、面向对象

1. 面向过程语言和面向对象语言的区别

面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发

面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要低。

2.java面向对象的理解?面向对象的特征?

整体上是封装、继承、多态、抽象。

首先面向对象是一种思想。在java中万事万物皆对象。类是对相同事物的一种抽象、是不可见的,对象具体的、可见的。由对象到类的过程是抽象的过程,由类到对象的过程是实例化的过程。面向对象的三大特征分别是封装、继承和多态。 

封装隐藏了类的内部实现机制,对外界而言它的内部细节是隐藏的,暴露给外界的只是它的访问方法。例如在属性的修饰符上我们往往用的private私有的,这样其它类要想访问就通过get和set方法。因此封装可以程序员按照既定的方式调用方法,不必关心方法的内部实现,便于使用; 便于修改,增强 代码的可维护性。 

继承在本质上是特殊~一般的关系,即常说的is-a关系。子类继承父类,表明子类是一种特殊的父类,并且具有父类所不具有的一些属性或方法。比如从猫类、狗类中可以抽象出一个动物类,具有和猫、狗、虎类的共同特性(吃、跑、叫等)。通过extends关键字来实现继承。Java中的继承是单继承,即一个子类只允许有一个父类。

 Java多态是指的是首先两个类有继承关系,其次子类重写了父类的方法,最后父类引用指向子类对象。如Animal a=new Dog();这行代码就体现了多态。

总是:

  • 封装:把客观事物封装成抽象的类,封装可以隐藏实现细节,使得代码模块化
  • 继承:继承是指可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。创建的新类称为“子类”或“派生类”。被继承的类称为“基类”、“父类”或“超类”。目的也是为了代码重用。
  • 多态:包括覆盖(重写)和重载,重写,是指子类重新定义父类的方法。重载,多个方法同名不同参(参数个数、参数类型等)。

3.介绍一下Object类中的方法?

(1). getClass 方法

方法: public final native Class<?> getClass();

描述:final 方法、获取对象的运行时 class 对象,class 对象就是描述对象所属类的对象。

这个方法通常是和 Java 反射机制搭配使用的。

(2). hashCode 方法

方法:public native int hashCode();

描述:该方法主要用于获取对象的散列值。Object 中该方法默认返回的是对象的堆内存地址。

(3). equals 方法

方法:public boolean equals(Object obj) { return (this == obj);}

描述:该方法用于比较两个对象,如果这两个对象引用指向的是同一个对象,那么返回 true,否则返回 false。一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。

重写equals为什么还要重写hashcode:

1)、为了保证一个原则,equals相同的两个对象hashcode必须相同。如果重写了equals而没有重写hashcode,会出现equals相同hashcode不相同这个现象。

2)、在散列集合中,是使用hashcode来计算key应存储在hash表的索引,如果重写了equals而没有重写hashcode,会出现两个完全相同的两个对象,hashcode不同,计算出的索引不同,那么这些集合就乱套了。

3)、提高效率,当我们比较两个对象是否相同的时候,先比较hashcode是否相同,如果hashcode不相同肯定不是一个对象,如果hashcode相同再调用equals来进行比较,减少比较次数提高效率。

(4). clone 方法

方法:protected native Object clone() throws CloneNotSupportedException;

描述:该方法是保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常。

当你使用 clone() 方法克隆一个对象时,会创建一个新的对象,但是对象的引用类型字段仍然指向相同的对象。

而对于基本类型字段和String,Java 中的自动装箱和拆箱特性会使得对象在被克隆时被重新分配内存,从而修改副本对象中的该字段不会影响原对象。

(5). toString 方法

方法:public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }

描述:返回一个 String 对象,一般子类都有覆盖。默认返回格式如下:对象的 class 名称 + @ + hashCode 的十六进制字符串。

. notify 方法

方法:public final native void notify();

描述:final 方法,主要用于唤醒在该对象上等待的某个线程。

(7). notifyAll 方法

方法:public final native void notifyAll();

描述:final 方法,主要用于唤醒在该对象上等待的所有线程。

(8). wait(long timeout)

方法:public final native void wait(long timeout) throws InterruptedException;

描述:wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait() 方法一直等待,直到获得锁或者被中断。wait(long timeout) 设定一个超时间隔,如果在规定时间内没有获得锁就返回。

(9). wait(long timeout, int nanos)

方法:

public final void wait(long timeout, int nanos) throws InterruptedException {

if (timeout < 0) {

throw new IllegalArgumentException("timeout value is negative");

}

if (nanos < 0 || nanos > 999999) {

throw new IllegalArgumentException( "nanosecond timeout value out of range");

}

if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {

timeout++;

}

wait(timeout);

}

描述:

timeout:最大等待时间(毫秒)

nanos:附加时间在毫秒范围(0-999999)

该方法导致当前线程等待,直到其他线程调用此对象的 notify() 方法或notifyAll()方法,或在指定已经过去的时间。此方法类似于 wait 方法的一个参数,但它允许更好地控制的时间等待一个通知放弃之前的量。实时量,以毫微秒计算,计算公式如下:1000000 * timeout + nanos

在所有其他方面,这种方法与 wait(long timeout) 做同样的事情。特别是 wait(0, 0) 表示和 wait(0) 相同。

(10). wait 方法

方法:public final void wait() throws InterruptedException { wait(0);}

描述:可以看到 wait() 方法实际上调用的是 wait(long timeout) 方法,只不过 timeout 为 0,即不等待。

(11). finalize 方法

方法:protected void finalize() throws Throwable { }

描述:该方法是保护方法,主要用于在 GC 的时候再次被调用,如果我们实现了这个方法,对象可能在这个方法中再次复活,从而避免被 GC 回收。

4.Java中实现多态的机制是什么?

Java中的多态靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程 序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存 里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

5.this与super的区别?

super:它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名 (实参)

this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)

super()和this()区别:

[1]super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。

[2]super()和this()均需放在构造方法内第一行。尽管可以用this调用一个构造器,但却不能调用两个。

[3]this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语 句,就失去了语句的意义,编译器也不会通过。

[4]this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

6.Java中重写和重载有哪些区别?

方法的重载和重写本质都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态。

方法重载的规则:

1)方法名一致,

2)参数列表不同(参数顺序不同或者参数类型不同或者参数个数不同)。

3)重载与方法的返回值无关,这个很关键。

方法重写的规则:

1)参数列表和返回值类型必须完全与父类的方法一致。

2)构造方法不能被重写,声明为 final 的方法不能被重写,声明为 static 的方法不能被重写,但是能够被再次声明。

3)访问权限不能比父类中被重写的方法的访问权限更低。

4)重写的方法能够抛出任何检查异常(编译时异常),但是重写的方法不能抛出比被重写方法声明的更广泛的运行时异常。

7.接口和抽象类有哪些区别?

抽象类

抽象类:使用abstract修饰;不能实例化;含有抽象方法的类是抽象类;抽象类可以含有抽象方法,也可以不包含抽象方法,抽象类中可以有具体的方法;如果一个子类实现了父类(抽象类)的所有抽象方法,那么该子类可以不必是抽象类,否则就是抽象类;

接口

接口:接口使用interface修饰;接口不能被实例化;一个类只能继承一个类,但是可以实现多个接口;

在jdk8之前,interface之中可以定义变量和方法,变量必须是public、static、final的,方法必须是public、abstract的。

JDK8及以后,允许我们在接口中定义static方法和default方法。

如果一个实现类实现了两个接口,两个接口中有同名的静态方法,不会产生错误,因为jdk8只能通过接口类调用接口中的静态方法,所以对编译器来说是可以区分的,接口中的静态方法可以在接口默认方法中调用,实现类的方法可以调用,但是不能通过实现类名及实现类对象来调用。

如果两个接口中定义了一模一样的默认方法,并且一个实现类同时实现了这两个接口,那么必须在实现类中重写默认方法,否则编译失败。

接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用。

8.接口是否可继承(extends)接口? 抽象类是否可实现(implements) 接口? 抽象类是否可继承具体类(concrete class)?

接口可以继承接口。抽象类可以实现(implements)接口,抽象类可继承具体类,但前提是具体类必须有明确的构造函数。

9.描述一下值传递和引用传递的区别?

值传递是指在调用函数时将实际参数复制一份到函数中,这样的话如果函数对其传递过来的形式参数进行修改,将不会影响到实际参数。

引用传递是指在调用函数时将对象的地址直接传递到函数中,如果在对形式参数进行修改,将影响到实际参数的值。

10.Java中==和equals有哪些区别?

equals 和== 最大的区别是一个是方法一个是运算符。1)基本类型中,==比较的是数值是否相等。equals方法是不能用于基本数据类型数据比较的,因为基本数据类型压根就没有方法。2)引用类型中,==比较的是对象的地址值是否相等。equals方法比较的是引用类型的变量所指向的对象的地址是否相等。应为String这个类重写了equals方法,比较的是字符串的内容。

11.hashCode()方法的作用?

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码 的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就 意味着Java中的任何类都包含有hashCode()函数。

12.hashcode方法和equals方法区别?

在Java中,每个对象都可以调用自己的hashCode方法得到自己的哈希值(hashCode),相当于对象的指纹信息,通常说世界上没有完全一样的指纹,但是在Java中没有这么绝对,我们依然可以用hashCode值来做一些提前的判断。

1)如果两个对象的hashCode值不一样,那么他们肯定是不同的两个对象;

2)如果两个对象的hashCode值一样,也不代表就是同一个对象;

3)如果两个对象的equals方法相等,那么他们的hashCode值一定相等。

在Java的一些集合类的实现中,在比较两个对象的值是否相等的时候,会根据上面的基本原则,先调用对象的hashCode值来进行比较,如果hashCode值不一样,就可以认定这是两个不一样的数据,如果hashCode值相同,我们会进一步调用equals()方法进行内容的比较。

13.为什么重写 equals 方法必须重写 hashcode 方法?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

思考:重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。

总结

  • equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
  • 两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。

14.两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?

不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。

Java对于eqauls方法和hashCode方法是这样规定的:

(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;

(2)如果两个对象的hashCode相同,它们并不一定相同。

当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对 象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如 果哈希码频繁的冲突将会造成存取性能急剧下降)。

15.有没有可能两个不相等的对象有相同的hashcode

能.在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值.当hash冲突产生时,一般

有以下几种方式来处理:

  • 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.
  • 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
  • 再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突

16.抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被 synchronized?

都不能。

1)抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。

2)本地方法是由本地代码(如 C++ 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。

3)synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。

17.final关键字的用法?

修饰类:当用final修饰一个类时,表明这个类不能被继承。正如String类是不能被继承的。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

修饰方法:使用final修饰方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。因此,只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final。(注:一个类中的private方法会隐式地被指定为final方法)

修饰变量:对于被final修饰的变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。虽然不能再指向其他对象,但是它指向的对象的内容是可变的。

18.final与finally、finalize 的区别?

1)final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。

2)finally:异常处理语句结构的一部分,表示总是执行。

3)finalize:Object 类的一个方法,当java对象没有更多的引用指向的时候,系统会自动的由垃圾回收器来负责调用此方法进行回收前的准备工作和垃圾回收。

19.静态变量和实例变量区别?

静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的 加载过程中,JVM只为静态变量分配一次内存空间。

实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象 的,在内存中,创建几次对象,就有几份成员变量。

20.静态方法和实例方法有何不同?

静态方法和实例方法的区别主要体现在两个方面:

在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的 方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。

静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法), 而不允许访问实例成员变量和实例方法;实例方法则无此限制。

21.访问修饰符public,private,protected,以及不写(默认)时的区别?

修饰符

当前类

同包

子类

其它包

public

protected

默认(缺省)

private

类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开 (public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对 子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。Java中,外部类的修饰符 只能是public或默认,类的成员(包括内部类)的修饰符可以是以上四种。

22. abstract的方法是否可同时是final、static、native或者synchronized的?

  • final修饰方法表示方法不能被子类重写,但是抽象方法需要被子类重写,所以肯定不能在一起使用。
  • abstract的method 不可以是static的,因为抽象的方法是要被子类实现的,而static与子类扯不上关系!
  • native方法表示该方法要用另外一种依赖平台的编程语言实现的,不存在着被子类实现的问题,所以它也不能是抽象的,不能与abstract混用。
  • abstract是抽象的,而synchronized是同步的,相对于线程讲的;abstract只是声明没有实现,既然没有实现就谈不上同步了,所以不能放到一块使用。当然如果其子类实现了这个方法在子类是可以同步的。

23. 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?

(1)引用拷贝: 引用拷贝不会在堆上创建一个新的对象,只 会在栈上生成一个新的引用地址,最终指向依然是堆上的同一个对象。 (2) 浅拷贝 :浅拷贝会在堆上创建一个新的对象,新对象和原对象不等,但是新对象的属性和老对象相同

其中:

a.如果属性是基本类型(int,double,long,boolean等),拷贝的就是基本类型的值。

b.如果属性是引用类型(除了基本类型都是引用类型),拷贝的就是引⽤数据类型变量的地址值,

⽽对于引⽤类型变量指向的堆中的对象不会拷贝。

(3) 深拷贝 :完全拷贝⼀个对象,在堆上创建一个新的对象,拷贝被拷贝对象的成员变量的值,同时堆中的对象也会拷贝。

24. 介绍下内部类

在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这三种:成员内部类、局部内部类、匿名内部类,如下图所示:

25.Java 创建对象有几种方式?

(1)new 关键字

  平时使用的最多的创建对象方式

User user=new User();
​

(2)反射方式

  使用 newInstance(),但是得处理两个异常 InstantiationException、IllegalAccessException:

User user=User.class.newInstance();
Object object=(Object)Class.forName("java.lang.Object").newInstance()
​

(3)clone方法

  Object对象中的clone方法来完成这个操作

(4)反序列化操作

  调用 ObjectInputStream 类的 readObject() 方法。我们反序列化一个对象,JVM 会给我们创建一个单独的对象。JVM 创建对象并不会调用任何构造函数。一个对象实现了 Serializable 接口,就可以把对象写入到文中,并通过读取文件来创建对象。

总结

  创建对象的方式关键字:new、反射、clone 拷贝、反序列化。

26. 内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制

一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。

27. Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以 实现接口

可以继承其他类或实现其他接口,在 Swing 编程中常用此方式来实现事件监听和回调。

28.什么是向上转型和向下转型?

  • 向上转型:子类转父类,自动、安全,只能用父类方法,多态核心
  • 向下转型:父类转子类,手动强转、有风险,必须用 instanceof 校验
  • 向下转型的前提:这个父类引用,原本就是子类对象向上转型来的
  • 一、向上转型(Upcasting)

    (1)定义

    子类对象 → 赋值给 → 父类引用Java 会自动完成,不需要手动强转,永远安全。

    (2)核心特点

    a.自动类型转换,无需强转

    b.只能调用父类定义的方法 / 属性

    c.若子类重写了方法,执行子类的重写版本(多态的体现)

    (3)代码示例

    // 父类
    class Animal {
        public void eat() {
            System.out.println("动物吃东西");
        }
    }
    
    // 子类
    class Dog extends Animal {
        // 重写父类方法
        @Override
        public void eat() {
            System.out.println("小狗吃骨头");
        }
        
        // 子类独有方法
        public void run() {
            System.out.println("小狗跑");
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            // 向上转型:子类对象 → 父类引用(自动转换)
            Animal animal = new Dog(); 
            
            animal.eat(); // 执行子类重写的方法 → 输出:小狗吃骨头
            // animal.run(); ❌ 报错!向上转型后,不能调用子类独有方法
        }
    }
    

    (4)用途

    最常用在方法参数,实现通用代码:

    // 一个方法接收所有 Animal 子类(猫、狗、猪...)
    public void feed(Animal animal) {
        animal.eat();
    }
    // 调用:直接传子类对象,自动向上转型
    feed(new Dog());
    feed(new Cat());
    

    二、向下转型(Downcasting)

    (1)定义

    父类引用 → 转换回 → 子类对象必须手动强制转换,有风险(可能转换失败报错)。

    (2)核心特点

    a.必须用 (子类类型) 手动强转

    b.只能转换原本就是这个子类的对象

    c.转换成功后,可以调用子类所有方法

    d.类型不匹配会抛出 ClassCastException 类型转换异常

    (3)代码示例

    public static void main(String[] args) {
        // 第一步:先向上转型
        Animal animal = new Dog(); 
        
        // 第二步:向下转型(手动强转)
        Dog dog = (Dog) animal; 
        
        dog.run(); // ✅ 成功!可以调用子类独有方法
    }
    

    (4)不安全的情况(必看)

    // 父类引用指向 Cat 对象
    Animal animal = new Cat();
    
    // 强转成 Dog → ❌ 运行报错:ClassCastException
    Dog dog = (Dog) animal; 
    

    (5)安全写法:用 instanceof 判断

    if (animal instanceof Dog) {
        Dog dog = (Dog) animal;
        dog.run();
    }
    

    29.BIO、NIO、AIO之间有什么区别?

    在计算机中,IO 传输数据有三种工作方式,分别是: BIO、NIO、AIO

    同步与异步的区别

    • 同步就是发起一个请求后,接受者未处理完请求之前,不返回结果。
    • 异步就是发起一个请求后,立刻得到接受者的回应表示已接收到请求,但是接受者并没有处理完,接受者通常依靠事件回调等机制来通知请求者其处理结果。

    阻塞和非阻塞的区别

    • 阻塞就是请求者发起一个请求,一直等待其请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
    • 非阻塞就是请求者发起一个请求,不用一直等着结果返回,可以先去干其他事情,当条件就绪的时候,就自动回来。

    模型

    全称

    核心特点

    通俗比喻

    适用场景

    BIO

    同步阻塞 IO

    一个连接 = 一个线程;全程阻塞

    去餐厅,

    一直等服务员上菜,啥也不干

    连接数少、并发低、简单业务。高并发会炸线程

    NIO

    同步非阻塞 IO

    一个线程处理多个连接;IO 不阻塞,主动询问

    去餐厅,

    边玩手机边问服务员好了没

    高并发、长连接、高性能(Netty 底层就是这个)

    AIO

    异步非阻塞 IO

    一个线程处理所有;IO 完成后主动通知

    去餐厅,

    留个电话,做好了服务员打给你

    高并发、连接极多,但Java 生态不如 NIO 成熟,实际很少手写 AIO,一般用 Netty (NIO)

    (1)、逐字拆解:核心区别

    a. BIO(Blocking IO)- 同步阻塞

    • 工作方式:客户端发起请求 → 服务端创建专属线程处理 → 线程全程阻塞,直到 IO 完成(读 / 写),期间啥也干不了。
    • 致命缺点:并发高时,线程数爆炸,内存 / CPU 扛不住,不适合高并发
    • 代码特点InputStreamOutputStream 原生 IO。

    特点:一个客户端连接,就开一个新线程。

    // BIO 服务端
    public class BioServer {
        public static void main(String[] args) throws IOException {
            ServerSocket serverSocket = new ServerSocket(8080);
            while (true) {
                // 阻塞,直到有连接
                Socket socket = serverSocket.accept();
                
                // 每来一个连接,开一个新线程
                new Thread(() -> {
                    try {
                        byte[] buf = new byte[1024];
                        int len = socket.getInputStream().read(buf); // 阻塞读
                        System.out.println("BIO 收到:" + new String(buf, 0, len));
                    } catch (IOException e) {e.printStackTrace();}
                }).start();
            }
        }
    }
    

    b. NIO(Non-blocking IO)- 同步非阻塞

    • 工作方式:用Selector(选择器) 监听所有连接,一个线程管理成千上万个连接;IO 操作不阻塞,线程主动轮询:"数据准备好了吗?",没准备好就去处理其他连接。
    • 核心组件Channel(通道)、Buffer(缓冲区)、Selector(选择器)。
    • 优势:线程数极少,高并发首选(Netty 就是基于 NIO)。

    特点:一个线程(Selector)管理成千上万连接。

    // NIO 服务端
    public class NioServer {
        public static void main(String[] args) throws IOException {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.bind(new InetSocketAddress(8080));
            ssc.configureBlocking(false); // 非阻塞
    
            Selector selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
    
            while (true) {
                selector.select(); // 阻塞等待事件
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
    
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    it.remove();
    
                    if (key.isAcceptable()) {
                        // 接受连接
                        ServerSocketChannel s = (ServerSocketChannel) key.channel();
                        SocketChannel sc = s.accept();
                        sc.configureBlocking(false);
                        sc.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        // 读数据
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buf = ByteBuffer.allocate(1024);
                        int len = sc.read(buf);
                        if (len > 0) {
                            System.out.println("NIO 收到:" + new String(buf.array(), 0, len));
                        }
                    }
                }
            }
        }
    }
    

    c. AIO(Asynchronous IO)- 异步非阻塞

    • 工作方式:发起 IO 请求 → 直接返回,完全不阻塞;操作系统完成 IO 后,主动通知服务端调用回调函数处理结果,不用自己轮询。
    • 特点:真正的异步,CPU 利用率最高。
    • 现状:Java 中成熟度不如 NIO,高并发场景用得少。

    特点:完全异步,OS 完成后回调通知,不用自己轮询。

    // AIO 服务端
    public class AioServer {
        public static void main(String[] args) throws IOException {
            AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
            server.bind(new InetSocketAddress(8080));
    
            // 异步接受连接,完成后回调
            server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
                @Override
                public void completed(AsynchronousSocketChannel client, Void attachment) {
                    // 继续接受下一个连接
                    server.accept(null, this);
    
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    // 异步读
                    client.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer len, ByteBuffer buffer) {
                            buffer.flip();
                            System.out.println("AIO 收到:" + new String(buffer.array(), 0, len));
                        }
                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {}
                    });
                }
                @Override
                public void failed(Throwable exc, Void attachment) {}
            });
    
            System.in.read(); // 保持程序不退出
        }
    }
    

    (2)、最核心的 3 个区别(面试必背)

    a.阻塞 vs 非阻塞BIO:

    BIO:阻塞,等 IO 完成才能干别的;

    NIO/AIO:非阻塞,发起 IO 后立刻去做其他事。

    b.同步 vs 异步同步(BIO/NIO):

    同步(BIO/NIO):自己主动检查 / 处理 IO 结果

    异步(AIO):操作系统通知你,不用自己管。

    c.线程模型:

    BIO:一连接一线程

    NIO:一线程管理多连接

    AIO:极少线程,靠回调

    三、String

    1. String 是最基本的数据类型吗?

    不是。Java 中的基本数据类型只有 8 个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type)外,剩下的都是引用类型(reference type)。

    2. String、StringBuffer、StringBuilder区别及使用场景?String 为什么是不可变的?

    都可以储存和操作字符串,String类表示内容不可改变的字符串,而StringBuffer和StringBuilder类都表示内容可以被修改的字符串。

    (1)可变性:String 不可变,后两者可变;

    (2)线程安全:String 天然安全、StringBuffer 安全(加锁)所有方法都使用synchronized关键字保证线程安全

    StringBuilder 不安全;

    (3)性能:StringBuilder > StringBuffer > String;

    (4)底层:String 用 final char [],后两者继承 AbstractStringBuilder 用可变 char []。

    2.1为什么 String 不可变,另外两个可变?

    String 用private final char[]存储数据一旦创建就不能被修改

    StringBuilder/StringBuffer 继承父类用无 final 的 char [],支持动态扩容和修改。

    2.2 字符串拼接用"+"还是StringBuilder?

  • 编译期确定的常量拼接:用"+"(编译器优化)
  • 循环内的动态拼接:必须用StringBuilder
  • 简单的少量拼接:可以用"+"(可读性好)
  • 2.3 JDK 9中String有什么变化?

    JDK 9将char[]改为byte[]+编码标志位:

    • 拉丁字符使用1字节存储,中文使用2字节
    • 显著减少内存占用(特别是英文文本)
    • 保持了相同的API,对开发者透明

    2.4 什么是 StringBuilder 的扩容机制?

    • 默认容量 16,超出后自动扩容为原容量 * 2 + 2,通过Arrays.copyOf复制数组。

    3. String str = “i” 和String str = new String(“1”)一样吗?

    不一样,因为内存的分配方式不一样。String str = "i"的方式JVM会将其分配到常量池中,而 String str = new String("i")JVM会将其分配到堆内存中。

    4. 是否可以继承String类?

    String 类是final类,不可以被继承。

    补充:继承String本身就是一个错误的行为,对String类型最好的重用方式是关联关系 (Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)。

    5. String s=new String(“xyz”);创建了几个字符串对象

    两个对象,

    (1) "xyz" - 字符串常量,存入字符串常量池 。

    (2)new String("xyz") - 在堆中新建的String对象。

    6. String类的常用方法有哪些?

    indexof();返回指定字符的的索引。

    charAt();返回指定索引处的字符。

    replace();字符串替换。

    trim();去除字符串两端空格。

    splt();字符串分割,返回分割后的字符串数组。

    getBytes();返回字符串byte类型数组。

    length();返回字符串长度。

    toLowerCase();将字符串转换为小写字母。 

    toUpperCase();将字符串转换为大写字母。

    substring();字符串截取。

    equals();比较字符串是否相等。

    7. 数组有没有 length()方法?String 有没有 length()方法?

    数组没有 length()方法,有 length 的属性。String 有 length()方法。JavaScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java混淆

    8. 怎样将 GB2312 编码的字符串转换为 ISO-8859-1 编码的字符串?

    String s1 = "你好"; String s2 = newString(s1.getBytes("GB2312"), "ISO-8859-1");

    9. String 中的 equals 是如何重写的?

    首先会判断要比较的两个字符串它们的引用是否相等。如果引用相等的话,直接返回 true ,不相等的话继续下面的判断,然后再判断被比较的对象是否是 String 的实例,如果不是的话直接返回 false,如果是的话,再比较两个字符串的长度是否相等,如果长度不想等的话也就没有比较的必要了;长度如果相同,会比较字符串中的每个 字符 是否相等,一旦有一个字符不相等,就会直接返回 false。

    10. 如何实现字符串的反转及替换?

    用递归实现字符串反转,代码如下所示:

    public static String reverse(String originStr) {
       if(originStr == null || originStr.length() <= 1)
           return originStr;
       return reverse(originStr.substring(1)) + originStr.charAt(0);
    }
    

    string.substring(from):相当于从from位置截取到原字符串末尾

    charAt() 方法用于返回指定索引处的字符。索引范围为从 0 到 length() - 1。

    11. 写一个函数,要求输入一个字符串和一个字符长度,对该字符串进行分隔

    public String[] split(String str, int chars){ 
        int n = (str.length()+ chars - 1)/chars; 
        String ret[] = new String[n]; 
        for(int i=0; i<n; i++){ 
            if(i < n-1){ 
                ret[i] = str.substring(i*chars , (i+1)*chars); 
            }else{ 
                ret[i] = str.substring(i*chars); 
            } 
        } 
        return ret; 
    } 
    

    12. 写一个函数,2 个参数,1 个字符串,1 个字节数,返回截取的字符串,要求字符串中的中文不能出现乱码:如(“我 ABC”,4)应该截为“我 AB”,输入(“我ABC 汉 DEF”,6)应该输出为“我 ABC”而不是“我 ABC+汉的半个”

    代码如下:

    public String subString(String str, int subBytes) { 
        int bytes = 0; // 用来存储字符串的总字节数 
        for (int i = 0; i < str.length(); i++) { 
            if (bytes == subBytes) { 
                return str.substring(0, i); 
            } 
            char c = str.charAt(i); 
            if (c < 256) { 
                bytes += 1; // 英文字符的字节数看作 1 
            } else { 
                bytes += 2; // 中文字符的字节数看作 2 
                if(bytes - subBytes == 1){ 
                    return str.substring(0, i); 
                } 
            } 
        } 
        return str; 
    }
    

    13. 用程序给出随便大小的 10 个数,序号为 1-10,按从小到大顺序输出,并输出相应的序号

    package test; 
    import java.util.ArrayList; 
    import java.util.Collections; 
    import java.util.Iterator; 
    import java.util.List; 
    import java.util.Random; 
    public class RandomSort { 
        public static void printRandomBySort() { 
            Random random = new Random(); // 创建随机数生成器 
            List list = new ArrayList(); // 生成 10 个随机数,并放在集合 list 中 
            for (int i = 0; i < 10; i++) { 
                list.add(random.nextInt(1000)); 
            } 
            Collections.sort(list); // 对集合中的元素进行排序 
            Iterator it = list.iterator(); 
            int count = 0; 
            while (it.hasNext()) { // 顺序输出排序后集合中的元素 
                System.out.println(++count + ": " + it.next()); 
            } 
        } 
        public static void main(String[] args) { 
            printRandomBySort(); 
        } 
    } 
    

    14. 写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数

    public int countWords(String file, String find) throws Exception { 
        int count = 0; 
        Reader in = new FileReader(file); 
        int c; 
        while ((c = in.read()) != -1) { 
            while (c == find.charAt(0)) { 
                for (int i = 1; i < find.length(); i++) { 
                    c = in.read(); 
                    if (c != find.charAt(i)) 
                        break; 
                    if (i == find.length() - 1) 
                        count++; 
                 } 
            } 
        } 
        return count; 
    }
    

    15. 如何把一段逗号分割的字符串转换成一个数组?

    方式1:使用String的split方法分割

    String str="aa,bb,cc,dd";
    String[] strArr = str.split(",");
    for (String s : strArr) {
        System.out.println(s);
    }
    

    方式2:使用StringTokenizer字符串分隔解析类

    构造器StringTokenizer(String str, String delim) :构造一个用来解析str的StringTokenizer对象,并提供一个指定的分隔符。

    boolean hasMoreTokens() :返回是否还有分隔符。

    String nextToken() :返回从当前位置到下一个分隔符的字符串。

    StringTokenizer st = new StringTokenizer(str,",");
    while(st.hasMoreTokens()){
        String s = st.nextToken();
        System.out.println(s);
    }
    

    16. 下面这条语句一共创建了多少个对象:String s="a"+"b"+"c"+"d";

    javac编译可以对字符串常量直接相加的表达式进行优化(编译期优化),不必要等到运行期去进行加法运算处理,而是在编译时去掉其中的加号,直接将其编译成一个这些常量相连的结果。上述代码被编译器在编译时优化后,相当于直接定义了一个 "abcd" 的字符串,所以只创建了一个String对象。

    验证:

    String s1 = "a";
    String s2 = s1 + "b";
    String s3 = "a" + "b";
    System.out.println(s2 == "ab");//false
    System.out.println(s3 == "ab");//true 
    

    四、集合

    1.集合框架中的泛型有什么优点?

    Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。

    2.Iterator是什么?

    Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。

    3.集合和数组的区别?

    数组是固定长度的;集合可变长度的。

    数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。

    数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

    4. 介绍Collection框架的结构

    java.util
    ├── Collection (根接口,单元素集合)
    │   ├── List    (有序、可重复、有索引)
    │   │   ├─ ArrayList
    │   │   ├─ LinkedList
    │   │   └─ Vector(过时)
    │   │
    │   ├── Set     (无序、不可重复、无索引)
    │   │   ├─ HashSet
    │   │   │   └─ LinkedHashSet
    │   │   └─ TreeSet
    │   │
    │   └── Queue   (队列,FIFO)
    │       ├─ LinkedList
    │       ├─ PriorityQueue
    │       └─ Deque (双端队列)
    │
    └── Map  (根接口,键值对集合,独立于 Collection)
        ├─ HashMap
        │   └─ LinkedHashMap
        ├─ TreeMap
        ├─ Hashtable(过时)
        └─ ConcurrentHashMap(JUC)
    

    一、List 家族(有序、可重复、有索引)

    (1). ArrayList

    元素特点:有序、可重复、有索引、查询快

    底层实现:动态数组

    扩容机制:默认容量 10,扩容为 1.5 倍

    优点:随机访问(get)速度极快

    缺点:增删需要移动元素,效率低

    线程安全:不安全

    (2). LinkedList

    元素特点:有序、可重复、有索引

    底层实现:双向链表

    优点:头尾增删效率极高

    缺点:随机查询慢(要遍历)

    线程安全:不安全

    (3). Vector

    元素特点:同 ArrayList

    底层实现:数组

    扩容:扩容 2 倍

    特点:方法加 synchronized,线程安全

    地位:过时,不用

    二、Set 家族(无序、不可重复、无索引)

    (1). HashSet

    元素特点:无序、不可重复、无索引

    底层实现:HashMap(数组 + 链表 + 红黑树)

    去重原理:hashCode() + equals()

    线程安全:不安全

    (2). LinkedHashSet

    元素特点:有序(插入顺序)、不可重复

    底层实现:LinkedHashMap(HashMap + 双向链表)

    作用:保证存取顺序一致

    (3). TreeSet

    元素特点:自动排序、不可重复

    底层实现:TreeMap(红黑树)

    排序:自然排序(Comparable)或定制排序(Comparator)

    三、Queue 家族(队列:FIFO)

    (1). LinkedList

    元素特点:FIFO 先进先出

    底层实现:双向链表

    用途:普通队列、双端队列

    (2). PriorityQueue

    元素特点:按优先级出队,非严格 FIFO

    底层实现:堆(二叉堆)

    排序:自然排序或定制排序

    (3). Deque

    特点:双端队列,头尾都可进出

    实现:LinkedList、ArrayDeque

    四、Map 家族(Key-Value、Key 唯一)

    (1). HashMap

    元素特点:Key 无序、Key 唯一、Value 可重复

    底层实现:数组 + 链表 + 红黑树(JDK1.8)

    链表转红黑树:长度 >8 转树,<6 退链表

    线程安全:不安全

    扩容:2 倍扩容

    (2). LinkedHashMap

    元素特点:保证插入顺序

    底层实现:HashMap + 双向链表

    用途:缓存、有序业务

    (3). TreeMap

    元素特点:Key自动排序

    底层实现:红黑树

    排序:按 Key 自然排序 / 定制排序

    (4). Hashtable

    特点:线程安全(synchronized)、效率低

    底层:数组 + 链表

    地位:过时

    (5). ConcurrentHashMap

    特点:高并发安全、效率高

    底层:1.7 分段锁;1.8 CAS + synchronized

    地位:多线程首选

    5. Collection框架中实现比较要实现什么接口?

    要排序的类实现Comparable接口或者通过其它实现Comparator接口的比较器来排序。

    Compaable接口通过compareTo()方法进行比较;Comparator接口通过compare()方法进行比较。

    一般排序使用Collections工具类的sort()方法对集合进行排序。

    Collections.sort(实现了Comparable接口的类的集合);//它会使用集合中元素的默认比较器,即元素必须实现Comparable接口

    Collections.sort(集合,外部比较器);

    6.常用的集合类有哪些?

    Map接口和Collection接口是所有集合框架的父接口:

    Collection接口的子接口包括:Set接口和List接口

    Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等

    Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

    List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

    Queue 接口的实现类主要有:LinkedList、PriorityQueue

    7.Collection 和 Collections 有什么区别?

    java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,

    其直接继承接口有List与Set。

    Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

    8.Set和List的区别?

    Set 接口实例存储的是无序的,不重复的数据。List 接口实例存储的是有序的,可以重复的元素。都可以存储null值,但是set不能重复所以最多只能有一个空元素。

    Set检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>。

    List和数组类似,可以动态增长,根据实际存储的数据的长度自动增长List的长度。查找元素效率高,插入删除效率低,因为会引起其他元素位置改变 <实现类有ArrayList,LinkedList,Vector> 。

    9.Arraylist与 LinkedList 异同?

    1)Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向循环链表数据结构;

    2)ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。插入末尾还好,如果是中间,则(add(int index, E element))接近O(n);LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。

    3)LinkedList 不支持高效的随机元素访问,而ArrayList 实现了RandmoAccess 接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。所以ArrayList随机访问快,插入慢;LinkedList随机访问慢,插入快。

    4)ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

    10.ArrayList和Vector有何异同点?

    ArrayList和Vector在很多时候都很类似。

    (1)两者都是基于索引的,内部由一个数组支持。

    (2)两者维护插入的顺序,我们可以根据插入顺序来获取元素。

    (3)ArrayList和Vector的迭代器实现都是fail-fast的。

    (4)ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。

    以下是ArrayList和Vector的不同点。

    (1)Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。

    (2)ArrayList比Vector快,它因为有同步,不会过载。

    (3)ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。

    11.遍历一个List有哪些不同的方式?

    List<String> strList = new ArrayList<>();
    //使用for-each循环
    for(String obj : strList){
      System.out.println(obj);
    }
    //using iterator
    Iterator<String> it = strList.iterator();
    while(it.hasNext()){
      String obj = it.next();
      System.out.println(obj);
    }
    

    12. List、Map、Set、Queue四个接口存取元素时,各有什么特点?

    List

    单个元素

    有序

    可重复

    索引

    存取,可随机访问

    有序列表、需要下标操作

    Set

    单个元素

    无序(LinkedHashSet 除外)

    不可重复

    自动去重,只能遍历,无索引

    去重、唯一集合

    Map

    Key-Value 键值对

    无序(LinkedHashMap 除外)

    Key 不可重复

    Value 可重复

    通过

    Key

    存取值,Key 重复覆盖

    映射、缓存、查找表

    Queue

    单个元素

    先进先出 (FIFO)

    可重复

    只允许

    队尾入、队头出

    ,典型队列结构

    排队、消息、任务调度

    13. 说出ArrayList,Vector, LinkedList存储性能和特性

    同步性:ArrayList,LinkedList是不同步的,而Vector是同步的,由于使用了synchronized方法(线程安全),性能上较ArrayList差。

    数据增长:从内部实现机制来讲ArrayList和Vector都是使用Object的数组形式来存储的。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%(即变为原来的1.5倍)。

    检索、插入、删除对象的效率:ArrayList和Vector中,从指定的位置(用index)检索一个对象,或在集合的末尾插入、删除一个对象的时间是一样的, 但是在集合的其他位置增加或移除元素花费时间长,由于要涉及数组元素移动等内存操作。

    LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。在插入、删除集合中任何位置的元素所花费的时间都是一样的,但它在索引一个元素的时候比较慢。

    14. LinkedList 为什么不能实现RandomAccess接口,ArrayList可以?

    ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的。

    15. Comparable 和 Comparator 的区别

    ComparableComparator 都是 Java 中用于定义对象比较规则的接口,但它们在使用方式和应用场景上存在显著的区别。

    一、定义与位置

  • Comparable 是 java.lang 包下的接口,只有一个方法 compareTo,需要实体类自己实现,属于内部比较器,提供默认排序规则。
  • Comparator 是 java.util 包下的接口,方法是 compare,不需要修改实体类,属于外部比较器,可以灵活实现多种排序。
  • 二、应用场景

    1. Comparable:

    ○ 适用于类本身具有自然排序规则的情况。

    ○ 例如,String 类和 Integer 类都实现了 Comparable 接口,因此它们可以按照字典序和数字大小进行自然排序。

    2. Comparator:

    ○ 适用于需要对对象进行多种排序规则的情况。

    ○ 例如,对于 Person 类,我们可以定义一个根据年龄排序的 Comparator 和一个根据姓名排序的 Comparator。

    ○ Comparator 还常用于对集合或数组进行排序时,当集合或数组中的元素类型没有实现 Comparable 接口,或者需要按照不同于自然排序规则的方式排序时。四、优先级

    • 当一个类同时实现了 Comparable 接口和提供了 Comparator 比较器时,通常 Comparator 的比较

    规则会覆盖 Comparable 的自然排序规则。

    • 这意味着在使用排序方法(如 Collections.sort()Arrays.sort())时,如果指定了 Comparator

    则会按照 Comparator 的规则进行排序;否则,会按照 Comparable 的自然排序规则进行排序。

    16.HashSet如何检查重复?

    当把对象加入到HashSet中时,HashSet会先计算对象的hashCode值来判断对象加入的下标位置,同时也会与其他的对象的hashCode进行比较,如果没有相同的,就直接插入数据;如果有相同的,就进一步使用equals来进行比较对象是否相同,如果相同,就不会加入成功。

    17.HashMap如何遍历?

    1.使用foreach循环遍历

    Map<String, String> hashMap = new HashMap<String,String>();
    hashMap.put("1", "good");
    hashMap.put("2", "study");
    hashMap.put("3", "day");
    hashMap.put("4", "up");
    for (Map.Entry<String, String> entry : hashMap.entrySet()) {
        System.out.println(entry.getKey()+":"+entry.getValue());
    }
    

    2.使用foreach迭代键值对

    Map<String, String> hashMap = new HashMap<String,String>();
    hashMap.put("1", "good");
    hashMap.put("2", "study");
    hashMap.put("3", "day");
    hashMap.put("4", "up");
    for (String key : hashMap.keySet()) {
        System.out.println(key);
    }
    for (String value : hashMap.values()) {
        System.out.println(value);
    }
    

    3.使用迭代器

    Map<String, String> hashMap = new HashMap<String,String>();
    hashMap.put("1", "good");
    hashMap.put("2", "study");
    hashMap.put("3", "day");
    hashMap.put("4", "up");
    Iterator<Map.Entry<String, String>> iterator = hashMap.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, String> next = iterator.next();
        System.out.println(next.getKey()+":"+next.getValue());
    }
    

    4.使用lambda表达式

    Map<String, String> hashMap = new HashMap<String,String>();
    hashMap.put("1", "good");
    hashMap.put("2", "study");
    hashMap.put("3", "day");
    hashMap.put("4", "up");
    hashMap.forEach((k,v)-> System.out.println(k+":"+v));
    

    18.HashMap 和 Hashtable 的区别?

    相同点:

    都是实现来Map接口(hashTable还实现了Dictionary 抽象类)。

    不同点:

    1. 历史原因:Hashtable 是基于陈旧的 Dictionary 类的,HashMap 是 Java 1.2 引进的 Map 接口

    的一个实现,HashMap把Hashtable 的contains方法去掉了,改成containsvalue 和containsKey。因为contains方法容易让人引起误解。

    2. 同步性:Hashtable 的方法是 Synchronize 的,线程安全;而 HashMap 是线程不安全的,不是同步的。所以只有一个线程的时候使用hashMap效率要高。

    3. 值:HashMap对象的key、value值均可为null。HahTable对象的key、value值均不可为null。

    4. 容量:HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。

    5. HashMap扩容时是当前容量翻倍即:capacity * 2,Hashtable扩容时是容量翻倍+1 即:capacity * 2+1。

    19.HashSet 和 HashMap 区别?

    HashSet 底层就是基于 HashMap 实现的。只不过HashSet里面的HashMap所有的value都是同一个Object而已,因此HashSet也是非线程安全的。

    20.HaspMap与TreeMap的区别?

    1. HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。

    2. 在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。

    21.ArrayList自动扩容?

    每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过ensureCapacity(int minCapacity)方法来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

    22.ArrayList的Fail-Fast机制?

    ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

    23.集合中的 fail-fast 和 fail-safe 是什么?

    fail-fast(快速失败)和 fail-safe(安全失败)是Java集合框架在处理并发修改问题

  • fail-fast(快速失败)是 Java 非线程安全集合的错误检测机制。迭代时如果集合被修改,会立刻抛出 ConcurrentModificationException。原理是比较 modCount 和迭代器的 expectedModCount。
  • fail-safe(安全失败)是 JUC 线程安全集合的机制。迭代时遍历集合副本,原集合修改不影响迭代,不会抛异常。但只能保证弱一致性,不能保证读到最新数据。
  • fail-fast 解决方案:

    1. 单线程:用 Iterator.remove()list.removeIf()
    2. 多线程:

    加锁:Collections.synchronizedList

    无锁安全:CopyOnWriteArrayList(fail-safe)

    fail-fast vs fail-safe

    • fail-fast:直接遍历原集合,修改抛异常。
    • fail-safe:遍历副本,不抛异常,弱一致性。

    24. ConcurrentHashMap了解吗?

    特性

    JDK 1.7

    JDK 1.8

    结构

    Segment + 数组 + 链表

    数组 + 链表 + 红黑树

    锁机制

    分段锁(ReentrantLock)

    CAS + synchronized

    锁粒度

    Segment(段)

    数组头节点(桶)

    并发度

    默认 16

    更高,每个桶一个锁

    哈希冲突

    链表

    链表 + 红黑树

    锁重量

    重锁

    低粒度、轻量锁

    性能

    极高

    复杂度

    简洁、清晰

    整体架构:

    • 底层是由数组+单向链表+ 红黑树组成。
    • 当初始化一个ConcurrentHahMap的时候,底层默认会初始化一个长度为16的数组。
    • 因为底层是哈希表结构,必然会存在哈希冲突的问题,采用链式寻址的方式来解决哈希表冲突问题。
    • 当哈希冲突比较多的时候,会造成链表长度较长的问题,这样就会增加查询复杂度,所以JDK1.8后引入了红黑树的机制,当数组长度大于64,并且链表的长度大于8的时候,单向链表就会转成红黑树。
    • 如果元素数量减少,一旦长度小于等于6,红黑树会退化成为单向链表。

    基本功能:

    • ConcurrentHahMap本质上还是一个HashMap,因此功能与HashMap是一样的,但是ConcurrentHahMap在HashMap的基础上,提供了并发安全的实现,如何实现?主要是通过对Node节点去加锁来保证数据更新的安全性:

    性能优化:

    • JDK1.7之前,它采用的是Segment锁,分段锁,锁的范围更大,所以性能上会更低(JDK1.7最多16把锁)
    • 在JDK1.8后,锁的粒度更细,一个桶是一把锁(一个桶代表一个索引位置,包括后面的链表或者红黑树),数据放数组时候,用的是CAS,数据放链表或者红黑树的时候,用的是synchronized
    • 引入红黑树机制后,降低了数据查询的时间复杂度,红黑树的时间复杂度是O(logn)
    • 当数组的长度不够的时候,底层需要对数组进行扩容,在扩容的方式上,引入了多线程并发扩容的实现.就是多个线程对原始数组进行分片分片后,每个线程负责一个分片的数据迁移,从而整体提升扩容过程中数据迁移的效率

    25.ConcurrentHashMap 、Hashtable 和HashMap 的区别

  • HashMap 线程不安全,效率最高,允许 null,适合单线程。
  • Hashtable 线程安全,使用 synchronized 锁住整张表,并发效率极低,不允许 null,已过时。
  • ConcurrentHashMap 线程安全,JDK1.8 使用 CAS + synchronized 桶头锁,锁粒度小,高并发性能好,不允许 null,是多线程场景下的首选
  • 特性

    HashMap

    Hashtable

    ConcurrentHashMap

    线程安全

    不安全

    安全

    安全

    锁机制

    无锁

    synchronized 全表锁

    JDK1.7:分段锁

    JDK1.8:CAS + synchronized 桶头锁

    锁粒度

    整个 Map

    一段 / 一个桶

    并发效率

    单线程最高

    极低(串行)

    极高(高并发)

    null key/value

    允许 1 个 null key,多个 null value

    不允许

    不允许

    底层结构

    数组 + 链表 + 红黑树

    数组 + 链表

    数组 + 链表 + 红黑树

    扩容

    单线程

    单线程

    多线程协同扩容

    推荐场景

    单线程

    不用,已过时

    多线程 / 高并发

    五、异常

    1.简单概述下异常。

    Throwable 是 Java 语言中所有错误与异常的超类。

    Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。 

    Exception 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。

    运行时异常 都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。 运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 

    非运行时异常 (编译异常)是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

    2.异常如何处理的?

    异常需要处理的时机分为编译时异常(也叫受控异常)也叫 CheckedException 和运行时异常(也叫非受控异常)也叫 UnCheckedException。Java认为Checked异常都是可以被处理的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked 异常,该程序在编译时就会发生错误无法编译。这体现了Java 的设计哲学:没有完善错误处理的代码根本没有机会被执行。

    对Checked异常处理方法有两种:

    第一种:当前方法知道如何处理该异常,则用try...catch块来处理该异常。

    第二种:当前方法不知道如何处理,则在定义该方法时声明抛出该异常。

    运行时异常只有当代码在运行时才发行的异常,编译的时候不需要try…catch。Runtime如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。

    3. throws、throw、try、catch、finally 分别如何使用?

    Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。

    一般情况下是用 try 来执行一段程序,如果出现异常,系统会抛出(throw)一个异常,这时候你可以通过它的类型来捕捉(catch)它,或最后(finally)由缺省处理器来处理;

    try 用来指定一块预防所有“异常”的程序;

    catch 子句紧跟在 try 块后面,用来指定你想要捕捉的“异常”的类型;

    throw 语句用来明确地抛出一个“异常”;

    throws 用来标明一个成员函数可能抛出的各种“异常”;

    finally 为确保一段代码不管发生什么“异常”都被执行一段代码;可以在一个成员函数调用的外面写一个 try 语句,在这个成员函数内部写另一个 try 语句保护其他代码。每当遇到一个 try 语句,“异常”的框架就放到栈上面,直到所有的try 语句都完成。如果下一级的 try 语句没有对某种"异常"进行处理,栈就会展开,直到遇到有处理这种"异常"的 try 语句。

    4.try{}里有一个 return 语句,那么紧跟在这个 try 后的 finally{}里的 code 会不会被执行,什么时候被执行,在 return 前还是后?

    会执行,在方法返回调用者前执行。Java 允许在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try 中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,这会对程序造成很大的困扰,C#中就从语法上规定不能做这样的事。

    5.Error 和 Exception 有什么区别?

    Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。

    6.下面代码的输出结果?

    public int getNum() { 
       try { 
          int a = 1 / 0; 
          return 1; 
       } catch (Exception e) {
          return 2; 
       } finally { 
          return 3; 
       } 
    }
    

    分析:代码走到第3行的时候遇到了一个MathException,这时第4行的代码就不会执行了,代码直接跳转到catch语句中,走到第 6 行的时候,异常机制有一个原则:如果在catch中遇到了return或者异常等能使该函数终止的话那么有finally就必须先执行完finally代码块里面的代码然后再返回值。因此代码又跳到第8行,可惜第8行是一个return语句,那么这个时候方法就结束了,因此第6行的返回结果就无法被真正返回。因此上面返回值是3。

    7.常见的异常有哪些?

    (1)java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。

    (2) java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。

    (3) java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。

    (4) java.lang.ClassCastException 数据类型转换异常。

    (5) java.lang.SQLException SQL异常,常见于操作数据库时的 SQL 语句错误。

    8.throw 和 throws 的区别?

    (1) throw:

    throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。

    throw是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行throw一定是抛出了某种异常。

    (2) throws:

    throws语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。

    throws主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。

    9.运行时异常与一般检查异常有何异同?

    异常表示程序运行过程中可能出现的非正常状态,Exception类根据错误发生的原因分为:RuntimeException异常和RuntimeException之外的异常。

    运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java编译器不要求必须声明抛出未被捕获的运行时异常,如NullPointerException,ClassCastException,IndexOutOfBoundsException等。

    非运行时异常也叫一般异常,java编译器要求方法必须声明抛出可能发生的非运行时异常,常见的一般异常包括如SocketException,SQLException,IOException,ClassNotFoundException,NoSuchMethodException等。

    10.try {}里有一个return语句,那么finally {}里的code会不会被执行?

    return和finally执行顺序是:先执行finally最后执行return。

    11.如何使用try-with-resources代替try-catch-finally?

    在Java中,try-with-resources语句是一种更简洁、更安全的方式来管理资源,它自动处理实现了AutoCloseable接口(或其子接口Closeable)的资源的关闭操作。这种方式可以替代传统的try-catch-finally结构,使得代码更加简洁且易于维护。

    try-with-resources语句确保每个资源在语句结束时自动关闭,无论是正常结束还是异常结束。它通过在try关键字后面使用一对圆括号来声明一个或多个资源。

    try-with-resources

    import java.io.BufferedReader;
    import java.io.FileReader;
    import java.io.IOException;
    
    public class TryWithResourcesDemo {
        public static void main(String[] args) {
            String filePath = "test.txt";
            try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
                String line;
                while ((line = br.readLine()) != null) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 注意:这里不需要显式地关闭BufferedReader,因为try-with-resources会自动处理
        }
    }
    

    try-catch-finally

    import java.io.BufferedReader;
    import java.io.FileReader;
    import java.io.IOException;
    
    public class TryCatchFinallyDemo {
        public static void main(String[] args) {
            String filePath = "test.txt";
            BufferedReader br = null;
            try {
                br = new BufferedReader(new FileReader(filePath));
                String line;
                while ((line = br.readLine()) != null) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (br != null) {
                    try {
                        br.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    12.try-with-resources 和 try-catch-finally 的区别

  • 资源关闭finally:必须手动在 finally 里关闭,还要判空、捕获异常。try-with-resources:自动调用 close(),不用写任何关闭代码。
  • 异常安全finally:如果 try 和 finally 都抛异常,前面的异常会丢失。try-with-resources:会把次要异常抑制,保留原始业务异常。
  • 代码简洁性finally:代码又长又难读。try-with-resources:优雅、清晰、不易出错。
  • 总结

  • try-catch-finally 需要手动在 finally 中关闭资源,代码臃肿,容易遗漏关闭,还可能出现异常覆盖。
  • try-with-resources 是 JDK7 新特性,实现 AutoCloseable 接口的资源会自动关闭,代码简洁、安全、无泄漏,是现代 Java 开发的标准写法。
  • 六、IO

    1.什么是IO流?

    IO流就是以流的方式进行输入输出。主要用来处理设备之间的传输,文件的上传,下载和复制。

    流分输入和输出,输入流从文件中读取数据存储到进程中,输出流从进程中读取数据然后写入到目标文件。

    2.java中有几种类型的流?这些流分别继承自哪些抽象类?

    按照流的方向:输入流(inputStream)和输出流(outputStream)

    按照实现功能分:节点流(可以从或向一个特定的地方(节点)读写数据。如 FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如 BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。)

    按照数据类型的单位: 字节流和字符流。字节流继承于 InputStream 和 OutputStream, 字符流继承于Reader 和 Writer 。

    InputStream
        ├── FileInputStream
        ├── BufferedInputStream
        ├── DataInputStream
        └── ...
    
    OutputStream
        ├── FileOutputStream
        ├── BufferedOutputStream
        ├── DataOutputStream
        └── ...
    
    Reader
        ├── FileReader
        ├── BufferedReader
        ├── InputStreamReader
        └── ...
    
    Writer
        ├── FileWriter
        ├── BufferedWriter
        ├── OutputStreamWriter
        └── ...
    

    3.字节流和字符流的区别?

    1)字节流读取的时候,读到一个字节就返回一个字节;字符流读取的时候会读到一个或多个字节(这个要根据字符流中编码设置,一般中文对应的字节数是两个,在UTF-8码表中是3个字节)

    2)字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。

    3)字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流在操作时使用了缓冲区,通过缓冲区再操作文件。

    案例1:在写操作的过程中,没有关闭字节流操作,但是文件中也依然存在了输出的内容代码如下:

    public static void main(String[] args) throws Exception { 
        // 第1步:使用File类找到一个文件 
        File f = new File("d:" + File.separator + "test.txt"); // 声明File 对象 
        // 第2步:通过子类实例化父类对象 
        OutputStream out = new FileOutputStream(f); 
        // 第3步:进行写操作 
        String str = "Hello World!!!"; // 准备一个字符串 
        byte b[] = str.getBytes(); // 字符串转byte数组 
        out.write(b); // 将内容输出 
        // 第4步:关闭输出流 
        // out.close();
    } 
    

    案例2:在写操作的过程中,没有关闭字符流操作,发现文件中没有任何内容输出。代码如下:

    public static void main(String[] args) throws Exception {         
        // 第1步:使用File类找到一个文件    
        File f = new File("d:" + File.separator + "test.txt");// 声明File 对象    
        // 第2步:通过子类实例化父类对象    
        Writer out = new FileWriter(f);            
        // 第3步:进行写操作    
        String str = "Hello World!!!"; // 准备一个字符串    
        out.write(str); // 将内容输出
        out.flush();     
        // 第4步:关闭输出流    
        // out.close();  
    }  
    

    这是因为字符流操作时使用了缓冲区,而在关闭字符流时会强制性地将缓冲区中的内容进行输出,但是如果程序没有关闭,则缓冲区中的内容是无法输出的。当然如果在不关闭字符流的情况下也可以使用Writer类中的flush()强制性的清空缓存,从而将字符流的内容全部输出。

    4.怎么样把字节流转换成字符流,说出它的步骤?

    解题思路:把字节流转成字符流就要用到适配器模式,需要用到OutputStreamWriter。它继承了Writer接口,但要创建它必须在构造函数中传入一个OutputStream的实例,OutputStreamWriter的作用也就是将OutputStream适配到Writer。它实现了Reader接口,并且持有了InputStream的引用。利用转换流OutputStreamWriter.创建一个字节流对象,将其作为参数传入转换流OutputStreamWriter中得到字符流对象.

    5.什么是序列化?

    序列化是指把对象转换为字节序列的过程,序列化后的字节流保存了对象的状态以及相关的描述信息,从而方便在网络上传输或者保存在本地文件中,达到对象状态的保存与重建的目的。

    反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

    序列化的优势:一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。三是通过序列化在进程间传递对象;

    6.IO如何实现序列化和反序列化?

    (1)java.io.ObjectOutputStream:表示对象输出流;它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;

    (2)java.io.ObjectInputStream:表示对象输入流;它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回;

    注意:只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!

    序列化和反序列化的示例

    public class SerialDemo {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
    	    //序列化
            FileOutputStream fos = new FileOutputStream("object.out");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            User user1 = new User("xuliugen", "123456", "male");
            oos.writeObject(user1);
            oos.flush();
            oos.close();
    		//反序列化
            FileInputStream fis = new FileInputStream("object.out");
            ObjectInputStream ois = new ObjectInputStream(fis);
            User user2 = (User) ois.readObject();
            System.out.println(user2.getUserName()+ " " + 
    	    user2.getPassword() + " " + user2.getSex());
            //反序列化的输出结果为:xuliugen 123456 male
        }
    }
    public class User implements Serializable {
        private String userName;
        private String password;
        private String sex;
        //全参构造方法、get和set方法省略
    }
    

    7.PrintStream、BufferedWriter、PrintWriter 的比较?

    1. PrintStream 类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包装成PrintStream 后进行输出。它还提供其他两项功能。与其他输出流不同,PrintStream 永远不会抛出 IOException;而是,异常情况仅设置可通过 checkError 方法测试的内部标志。另外,为了自动刷新,可以创建一个 PrintStream 

    2.BufferedWriter:将文本写入字符输出流,缓冲各个字符从而提供单个字符,数组和字符串的高效写入。通过 write()方法可以将获取到的字符输出,然后通过 newLine()进行换行操作。BufferedWriter 中的字符流必须通过调用 flush 方法才能将其刷出去。并且 BufferedWriter 只能对字符流进行操作。如果要对字节流操作,则使用 BufferedInputStream

    3.PrintWriter 的 println 方法自动添加换行,不会抛异常,若关心异常,需要调用 checkError方法看是否有异常发生,PrintWriter 构造方法可指定参数,实现自动刷新缓存(autoflush)。

    8.如果我要对字节流进行大量的从硬盘读取,要用那个流,为什么?

    因为明确说了是对字节流的读取,所以肯定是InputStream或者他的子类,又因为要大量读取,肯定要考虑到高效的问题,自然想到缓冲流BufferedInputStream。

    原因:BufferedInputStream是InputStream的缓冲流,使用它可以防止每次读取数据时进行实际的写操作,代表着使用缓冲区。不带缓冲的操作,每读一个字节就要写入一个字节,由于涉及磁盘的IO操作相比内存的操作要慢很多,所以不带缓冲的流效率很低。带缓冲的流,可以一次读很多字节,但不向磁盘中写入,只是先放到内存里。等凑够了缓冲区大小的时候一次性写入磁盘,这种方式可以减少磁盘操作次数,速度就会提高很多!并且也可以减少对磁盘的损伤。

    9.什么是java序列化和反序列化,如何实现?请解释Serializable接口的作用。 项目中哪些功能使用到了?

    (1)序列化和反序列化

    序列化是把 Java 对象转为字节流,反序列化是把字节流恢复成对象。

    (2)如何实现

  • 让类实现 Serializable 接口(标记接口,无方法)
  • 使用 ObjectOutputStream 序列化
  • 使用 ObjectInputStream 反序列化
  • // 1. 实现接口
    public class User implements Serializable {
        private String name;
        private int age;
    }
    
    // 2. 序列化(对象 → 字节流)
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"));
    oos.writeObject(user);
    
    // 3. 反序列化(字节流 → 对象)
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"));
    User user = (User) ois.readObject();
    

    (3)如Serializable 接口的作用

    • Serializable 是一个标记接口,里面没有任何方法。
    • 作用:告诉 JVM:这个类可以被序列化。没有实现这个接口的对象,序列化时会抛 NotSerializableException。

    扩展(加分点)

    • serialVersionUID作用:版本控制,反序列化时校验类是否一致,防止类修改后反序列化失败。

    (4)项目使用

    1. Redis 缓存对象

    • 把 Java 对象存入 Redis,必须序列化(JSON / 二进制 / JDK 序列化)。

    2. 网络传输(Dubbo / Spring Cloud / HTTP)

    • 微服务之间传递对象,需要序列化成字节 / JSON传输。

    3. 会话 Session 持久化

    • Tomcat 集群、Session 共享时,Session 里的对象要可序列化

    4. 分布式缓存 / 本地缓存

    • Caffeine、Guava Cache 存对象,有时需要序列化。

    5. RMI、远程调用、分布式锁值

    • 凡是跨进程、跨网络、存磁盘的对象,都要序列化。

    序列化 & 反序列化 注意事项(超全)

    1. 必须实现 Serializable 接口

    • 否则抛出 NotSerializableException
    • Serializable 是标记接口,无方法。

    2. 必须指定 serialVersionUID(强烈建议)

    • 不写:编译器自动生成,类一修改(加字段、改方法),UID 就变,反序列化直接失败
    • 手动写一个固定值,保证版本兼容。java运行

    3. transient 关键字修饰的属性不会被序列化

    • 临时变量、敏感信息(密码、密钥)用 transient
    • 反序列化后:引用类型 → null基本类型 → 默认值(0、false 等)

    4. 静态变量(static)不会被序列化

    • 序列化针对对象,static 属于类,不参与。

    5. 子类与父类的序列化规则

    • 子类实现 Serializable,父类没实现:父类属性不会序列化,反序列化时会调用父类无参构造重建。
    • 父类也实现,才会一起序列化。

    6. 被序列化的对象里的其他引用对象,也必须可序列化

    • 比如:java运行
    • 否则抛出异常。

    7. 单例类序列化会破坏单例

    • 反序列化会创建新对象,绕过构造器。
    • 解决:提供 readResolve() 方法返回单例。

    8. 安全性问题

    • 序列化数据可被篡改、伪造。
    • 敏感数据不要序列化,或加密后再存。

    9. 不推荐在分布式系统中大量使用 JDK 原生序列化

    • 跨语言差
    • 体积大
    • 性能一般
    • 有安全风险
    • 实际项目多用:JSON、Protobuf、Hessian、Kryo

      七、多线程

      1. 进程,线程,协程的区别

    (1)进程: 是正在运行中的程序,是系统进行资源调度和分配的的基本单位。每个进程有独立的内存空间。

    进程通讯就采用共享内存,MQ,管道。

    (2)线程: 是进程的子任务,是任务调度和执行的基本单位。一个线程只属于某一个进程。线程之间通讯,队列,await,signal,wait,notity,Exchanger,共享变量等等都可以实现线程之间的通讯。

    (3)协程:

    • 协程是一种用户态的轻量级线程。它是由程序员自行控制调度的。可以显示式的进行切换。
    • 一个线程可以调度多个协程。
    • 协程只存在于用户态,不存在线程中的用户态和内核态切换的问题。协程的挂起就好像线程的yield。
    • 可以基于协程避免使用锁这种机制来保证线程安全。

    单独的拿协程和线程做一个对比:

    • 更轻量: 线程一般占用的内存大小是MB级别。协程占用的内存大小是KB级别。
    • 简化并发问题: 协程咱们可以自己控制异步编程的执行顺序,协程就类似是串行的效果。
    • 减少上下文切换带来的性能损耗: 协程是用户态的,不存在线程挂起时用户态和内核态的切换,也不需要去让CPU记录切换点。
    • 协程优化的点: 协程在针对大量的IO密集操作时,协程可以更好有去优化这种业务。

    2. 并行和并发?

  • 并发:多个任务交替执行(单核切换)。
  • 并行:多个任务同时执行(多核)。
  • 3.多线程应用场景?

    当应用场景为计算密集型时:为了将每个cpu充分利用起来,线程数量正常是cpu核数+1,还可以看jdk的使用版本,1.8版本中可以使用cpu核数*2。

    当应用场景为io密集型时:做web端开发的时候,涉及到大量的网络传输,不进入持,缓存和与数据库交互也会存在大量io,当发生io时候,线程就会停止,等待io结束,数据准备好,线程才会继续执行,所以当io密集时,可以多创建点线程,让线程等待时候,其他线程执行,更高效的利用cpu效率,他有一个计算公式,套用公式的话,双核cpu理想的线程数就是20。

    4.使用多线程可能带来什么问题?

    (1). 线程安全问题(最核心)

    原子性破坏:复合操作(i++)不是原子的,多线程下会出现数据错乱。

    可见性问题:线程本地缓存,导致一个线程修改,另一个线程读不到。

    有序性问题:指令重排导致逻辑错误。

    结果:数据不一致、脏数据、重复扣款、超卖。

    解决方案

    • 保证原子性:synchronizedLockAtomicInteger 等原子类
    • 保证可见性 & 有序性:volatile
    • 线程安全集合:ConcurrentHashMapCopyOnWriteArrayList
    • 避免使用非安全类:SimpleDateFormat 换成 DateTimeFormatterHashMapConcurrentHashMap

    (2). 死锁(最严重)

    互斥、请求保持、不可剥夺、循环等待,四个条件同时满足就会死锁。

    结果:线程卡死、接口不响应、服务假死。

    解决方案

    • 固定顺序加锁
    • 使用 tryLock(timeout) 超时机制
    • 避免一个方法里嵌套多把锁
    • 线上排查:jstackjconsole

    (3). 活锁 & 饥饿

    活锁:线程不断重试,一直执行但永远完不成业务。

    饥饿:低优先级线程永远抢不到锁,一直不执行。

    解决方案

    • 随机等待时间,避免互相谦让
    • 使用公平锁new ReentrantLock(true)
    • 降低锁粒度,减少锁竞争

    (4). 上下文切换开销

    线程太多 → CPU 不停切换线程状态、保存现场。

    结果:CPU 使用率高,但业务吞吐量反而下降。

    解决方案

    • 必须用线程池,控制线程数量
    • CPU 密集型:CPU核心数+1
    • IO 密集型:2*CPU核心数
    • 减少锁竞争,用分段锁、无锁设计

    (5). 内存占用飙升/OOM

    每个线程都有独立栈空间(默认 1M)。

    线程过多 → 内存暴涨、OOM。

    解决方案

    • 限制最大线程数
    • 使用有界队列,拒绝溢出任务
    • 禁止用 Executors,手动创建 ThreadPoolExecutor

    (6). 线程泄漏/ 线程不释放

    线程池使用不当,线程一直不退出、不回收。

    结果:越来越多线程,最终打垮服务。

    解决方案

    • 强制使用线程池,禁止手动 new Thread()
    • 线程池里不允许使用死循环
    • 必须捕获异常,防止线程意外退出

    (7). 线程不安全的工具类引发灾难

    SimpleDateFormat、ArrayList、HashMap、StringBuilder 都是非线程安全。

    多线程共用会导致:数据丢、抛异常、结果错乱。

    (8). 线程优先级、信号、中断异常处理不当

    忽略 InterruptedException

    线程被强制中断导致数据不一致

    线程池里吞异常,问题无法排查

    (9). ThreadLocal 内存泄漏

    问题:线程复用,value 强引用无法回收

    解决方案

    • 使用完必须调用 remove ()
    • 定义为 static final ThreadLocal
    • 避免在线程池中存放大对象

    (10). 并发工具类使用不当

    CountDownLatch 忘记 countDown () → 永久阻塞

    Semaphore 忘记 release () → 许可证耗尽

    ThreadLocal 没 remove () → 内存泄漏

    解决方案

    • countDown()finally
    • release()finally
    • 超时机制,不无限等待

    (11). 线上排查难度剧增

    并发问题本地难复现、测试难测出、线上偶现。

    一旦出问题,定位成本极高。

    解决方案

    • 日志(关键变量、锁状态、时间点)
    • 压测模拟高并发
    • 使用 Arthasjstackjmap 线上诊断
    • 架构层面降低锁粒度、无锁化

    5.创建线程的几种方式?

    a.继承 Thread 类;

    b.实现 Runnable 接口;

    c. 实现Callable接口;

    d. 使用线程池;

    6.实现Runnable接口和Callable接口的区别?

    1. Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
    2. Callable规定的方法是call(),Runnable规定的方法是run()
    3. Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void)
    4. call方法可以抛出异常,run方法不可以
    5. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
    6. 加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。

    其实Callable接口底层的实现就是对Runable接口实现的封装,线程启动执行的也是Runable接口实现中的run方法,只是在run方法中有调用call方法罢了。

    7.如何停止一个正在运行的线程

    (1). 使用 volatile 标志位(推荐,适用于正常运行线程)

    • 定义一个 volatile boolean 变量作为停止标记。
    • 线程循环中判断标记,为 true 时退出。
    • volatile 保证多线程间可见性

    适用场景:线程一直在运行,没有阻塞。

    public class StopByFlag {
        private volatile boolean stop = false;
    
        public void stopThread() {
            stop = true;
        }
    
        public void runTask() {
            new Thread(() -> {
                while (!stop) {
                    System.out.println("线程运行中...");
                    try { Thread.sleep(200); } catch (Exception ignored) {}
                }
                System.out.println("线程通过标志位安全停止");
            }).start();
        }
    
        public static void main(String[] args) throws InterruptedException {
            StopByFlag demo = new StopByFlag();
            demo.runTask();
            Thread.sleep(1000);
            demo.stopThread();
        }
    }
    

    2. 使用 Thread.interrupt()(处理阻塞场景)

    • 调用 interrupt() 只是设置中断标志,不会立刻停止线程。
    • 当线程在 sleep()、wait()、join() 等阻塞时,会抛出 InterruptedException
    • 捕获异常后,即可安全退出。

    适用场景:线程可能阻塞,需要中断阻塞。

    public class StopByInterrupt {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("线程运行中...");
                    try {
                        Thread.sleep(500); // 阻塞时会被 interrupt() 打断
                    } catch (InterruptedException e) {
                        System.out.println("线程被中断,退出");
                        break;
                    }
                }
            });
            t.start();
            Thread.sleep(1000);
            t.interrupt(); // 触发中断
        }
    }
    

    3. 标志位 + interrupt () 结合(最健壮方案)

    • 循环条件同时判断:自定义停止标志Thread.currentThread().isInterrupted()
    • 既能处理正常运行,又能处理阻塞,最安全、最通用

    public class StopByMix {
        private volatile boolean stop = false;
    
        public void stop() {
            stop = true;
        }
    
        public void startTask() {
            Thread t = new Thread(() -> {
                while (!stop && !Thread.currentThread().isInterrupted()) {
                    System.out.println("运行中...");
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        System.out.println("阻塞被打断,退出");
                        break;
                    }
                }
                System.out.println("线程安全退出");
            });
            t.start();
    
            try { Thread.sleep(1000); } catch (Exception ignored) {}
            stop();
            t.interrupt();
        }
    
        public static void main(String[] args) {
            new StopByMix().startTask();
        }
    }
    

    4. Future.cancel()(线程池中的任务)

    • 提交到线程池的任务会返回 Future
    • 调用 future.cancel(true) 会尝试中断执行中的任务。
    • 本质还是通过 interrupt() 实现。

    import java.util.concurrent.*;
    
    public class StopByFutureCancel {
        public static void main(String[] args) throws InterruptedException {
            ExecutorService pool = Executors.newSingleThreadExecutor();
    
            Future<?> future = pool.submit(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("任务运行中...");
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        System.out.println("任务被中断取消");
                        break;
                    }
                }
            });
    
            Thread.sleep(1000);
            System.out.println("取消任务");
            future.cancel(true); // 相当于 interrupt()
    
            pool.shutdown();
        }
    }
    

    5. 绝对不推荐的方法

    (1)Thread.stop()已废弃,强制终止线程,不释放锁、不释放资源,极易造成数据不一致、死锁。

    (2)System.exit()直接退出 JVM,整个程序都停了,不是停止单个线程。

    5.1 Thread.stop ()(已废弃、暴力停止)

    new Thread(() -> {
        System.exit(0); // 直接退出虚拟机,所有线程全部死亡
    }).start();
    

    5.2 System.exit ()(杀死整个 JVM)

    // 危险!仅演示
    Thread t = new Thread(() -> {
        while (true) {
            // 可能持有锁,stop() 会直接杀死线程,锁永远不释放
        }
    });
    t.start();
    t.stop(); // 强制终止,不释放资源 → 数据错乱、死锁
    

    6.线程的 run()和 start()有什么区别?

    启动一个线程需要调用 Thread 对象的 start() 方法;调用线程的 start() 方法后,线程处于可运行状态,此时它可以由 JVM 调度并执行,这并不意味着线程就会立即运行;run() 方法是线程运行时由 JVM 回调的方法,无需手动写代码调用;直接调用线程的 run() 方法,相当于在调用线程里继续调用了一个普通的方法,并未启动一个新的线程。

    7.说说线程的生命周期和状态?

    状态

    核心说明

    关键触发动作

    NEW

    线程刚创建,未调用start(),未启动

    new Thread()

    RUNNABLE

    包含「就绪 (READY)」和「运行中 (RUNNING)」,等待 CPU 或正在执行

    start()、CPU 调度

    BLOCKED

    等待synchronized监视器锁,被动阻塞

    锁竞争失败

    WAITING

    主动无限等待,需其他线程唤醒

    wait()join()park()

    TIMED_WAITING

    主动限时等待,超时自动唤醒

    sleep(long)wait(long)join(long)

    TERMINATED

    线程执行完毕,生命周期终结

    任务完成 / 异常

    线程通常有五种状态:创建,就绪,运行,阻塞和死亡状态

    (1)创建状态(New):新创建了一个线程对象。

    (2)就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

    (3)运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

    (4)阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

    阻塞的情况分三种:

    (一)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

    (二)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

    (三)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

    (5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    8.什么是上下文切换?

    线程上下文切换是指操作系统为了能够让多个线程并发执行,在运行一个线程前,需要保存当前线程的 CPU 寄存器、程序计数器、栈指针和其他硬件上下文信息,以便于在恢复该线程时还原到之前的状态。而将这些信息保存起来、加载其他线程运行所需的上下文信息,然后再切换到该线程继续执行的过程就被称为线程上下文切换。

    (1)为什么要减少上下文切换

    • 上下文切换本身不执行业务逻辑,却要占用大量 CPU 时间,会导致系统吞吐量下降。
    • 切换过程需要频繁读写内存、保存 / 恢复现场,会降低 CPU 缓存命中率,带来额外性能损耗。
    • 线程过多会导致频繁切换,在多核 CPU 下还会加剧资源竞争,进一步拉低系统效率。

    (2)减少上下文切换的措施

  • 控制线程数量(核心):避免创建大量线程,结合业务类型合理设置线程数(CPU 密集型:CPU 核心数 + 1;IO 密集型:2*CPU 核心数),从源头降低线程间的竞争概率,减少切换。
  • 减少锁竞争(高频考点):使用细粒度锁(如 ConcurrentHashMap 的分段锁)、无锁编程(CAS 操作)、ThreadLocal(线程隔离,避免共享资源竞争),避免线程长时间阻塞和唤醒,减少切换触发场景。
  • 使用高效的同步机制:用 Lock(ReentrantLock)替代 synchronized 的低效场景,合理缩小同步块范围(避免锁范围过大导致线程长时间阻塞),降低线程阻塞与唤醒的频率,减少切换。
  • 使用线程池(实战必备):统一管理线程,通过线程池复用线程,避免频繁创建 / 销毁线程(线程创建 / 销毁本身也会触发上下文切换),减少不必要的调度与切换,同时控制线程池核心参数,避免线程过量。
  • 优化线程调度:合理设置线程优先级(避免低优先级线程长期饥饿),减少线程无效等待(如避免无限循环、减少 sleep/wait 的不合理使用),让 CPU 更专注于执行业务逻辑,降低切换频率。
  • 补充漏项(架构师级):采用协程(如 Java 的 VirtualThread 虚拟线程):协程切换开销远低于线程切换,适合高并发、轻量级任务,大幅减少上下文切换;避免无用阻塞:优化 IO 操作(如使用 NIO 非阻塞 IO),减少线程因 IO 阻塞导致的频繁切换。
  • 9.如何创建守护线程?

    使用Thread类的setDaemon(true)方法可以将线程设置为守护线程,需要注意的是,需要在调用start()方法前调用这个方法,否则会抛出IllegalThreadStateException异常。

    10.用户线程和守护线程有什么区别?

    当我们在Java程序中创建一个线程,它就被称为用户线程。一个守护线程是在后台执行并且不会阻止JVM终止的线程。当没有用户线程在运行的时候,JVM关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。

    一、定义

    1. 用户线程(User Thread)我们平时创建的普通线程,默认都是用户线程。JVM 会等待所有用户线程执行完毕才退出。
    2. 守护线程(Daemon Thread)为用户线程提供服务的后台线程(如 GC 线程)。当所有用户线程结束,JVM 直接退出,不管守护线程有没有执行完。

    二、核心区别

    1. JVM 退出规则不同用户线程:JVM 会等。守护线程:JVM 不等,用户线程全结束就直接退出。
    2. 用途不同用户线程:执行业务逻辑。守护线程:后台服务(GC、心跳、监控、日志刷新)。
    3. 设置时机必须在 start () 之前 设置:thread.setDaemon(true)启动后不能再修改守护状态。

    (1). 用户线程(默认)—— JVM 会等待结束

    public class UserThreadDemo {
        public static void main(String[] args) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    System.out.println("用户线程执行:" + i);
                    try { Thread.sleep(500); } catch (Exception e) {}
                }
                System.out.println("用户线程正常结束");
            });
    
            t.start(); // 默认是用户线程
            System.out.println("主线程执行完毕");
            // JVM 会等待用户线程执行完才退出
        }
    }
    

    (2). 守护线程 —— JVM 不会等待,用户线程一结束就退出

    public class DaemonThreadDemo {
        public static void main(String[] args) {
            Thread t = new Thread(() -> {
                while (true) { // 无限循环
                    System.out.println("守护线程后台运行...");
                    try { Thread.sleep(300); } catch (Exception e) {}
                }
            });
    
            t.setDaemon(true); // 设置为守护线程
            t.start(); // 必须在 start() 之前
    
            System.out.println("主线程休息 1 秒后退出");
            try { Thread.sleep(1000); } catch (Exception e) {}
            System.out.println("主线程结束,JVM 退出,守护线程直接被杀死");
        }
    }
    

    11.如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高?

    Windows系统下执行java  -jar  arthas-boot.jar

    Linux系统下解压arthas,执行ps  -ef  |  grep  java找出java进程pid数字

    12.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

    new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

    总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

    13.线程调度策略有哪些?

    线程调度策略是操作系统中管理线程执行顺序的关键机制

  • 抢占式调度(Preemptive):线程执行一段时间后,CPU 强制切换到其他线程。
  • 每个线程分配时间片。Windows / Linux / JVM 都用这个。

  • 协同式调度(Non-preemptive)( 时间片轮转):线程主动让出 CPU,才会切换。
  • 缺点:一个线程卡死,整个程序卡死。Java 不用。

    14.Java中用到的线程调度算法是什么?

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

    15.什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?

    线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。同上一个问题,线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是 更好的选择(也就是说不要让你的程序依赖于线程的优先级)。时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。 

    16.说说 sleep() 方法和 wait() 方法区别和共同点?

    两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。

    两者都可以暂停线程的执行。

    wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。

    wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。

    17.为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?

    因为这些方法的调用是依赖锁对象,而同步代码块的锁对象是任意。锁而Object代表任意的对象,所以定义在这里面。

    18.Java 线程数过多会造成什么异常?

    (1)线程的生命周期开销非常高

    (2)消耗过多的CPU 资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU 资源时还将产生其他性能的开销。

    (3)降低稳定性

    JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。

    19.多线程的常用方法?

    wait();(强迫一个线程等待)

    notify();(通知一个线程继续执行),

    notifyAll()(所有线程继续执行),

    sleep()(强迫一个线程睡眠N毫秒),

    join()(等待线程终止)

    yield()(线程让步)等等;

    20.线程之间如何进行通讯的?

    线程之间的通信有两种方式:共享内存和消息传递。

    (1)共享内存

      在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。

    例如线程A与线程B之间如果要通信的话,那么就必须经历下面两个步骤:

    1.线程A把本地内存A更新过得共享变量刷新到主内存中去。

    2.线程B到主内存中去读取线程A之前更新过的共享变量。

    (2)消息传递  

    在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在Java中典型的消息传递方式,就是wait()和notify(),或者BlockingQueue

    21.synchronized和ReentrantLock的区别

    相似点

      这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。

    区别

      这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

    synchronized 是 JVM 关键字,自动加解锁,非公平锁,功能简单,使用简单。

    ReentrantLock 是 JDK API,手动控制,支持公平锁、Condition 精确唤醒、中断、超时,功能更强大灵活。

    22.什么是线程安全

    多线程运行结果和单线程一致,不出现不确定值。

    线程安全根据安全程度可以分为四个级别:

    (1)不可变

    StringIntegerLong 这类 final 修饰的不可变类,一旦创建就不能被修改。

    任何线程都只能读,不能改,要修改只能新建对象。

    因此不可变对象天然线程安全,不需要任何同步机制

    (2)绝对线程安全

    无论在何种多线程运行环境下,调用者都不需要额外做同步,就能保证完全正确。

    实现绝对线程安全代价很高,Java 里真正属于这一类的很少,典型代表:

    • CopyOnWriteArrayList
    • CopyOnWriteArraySet

    (3)相对线程安全

    这就是我们平时所说的线程安全

    VectorHashTable,单个 add/remove 是原子操作,不会被打断。

    多个操作组合在一起时仍不安全,比如:

    • 一个线程遍历
    • 一个线程增删仍然会抛出 ConcurrentModificationException(fail-fast 机制)。

    (4)线程非安全

    内部没有任何同步保护,多线程并发修改会直接导致数据错乱、异常。

    典型代表:

    • ArrayList
    • LinkedList
    • HashMap

    23.线程安全需要保证几个基本特性?

    原子性简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。

    可见性:是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。

    有序性:是保证线程内串行语义,避免指令重排等。

    24.常用的线程池有哪些

    创建线程池可通过Java的ThreadPoolExecutor构造函数或Executors工具类实现

    一、Executors工具类实现

    (1)保证任务顺序执行(如事务处理)

    new SingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    (2)适用于负载稳定的场景(如 API 请求处理)

    new FixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

    (3)适用于大量短生命周期任务(如批量数据处理)

    new CachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

    (4)用于延迟或周期性任务调度(如心跳检测)

    new ScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的求。

    二、ThreadPoolExecutor构造函数实现(推荐生产环境)

    直接配置核心参数,灵活控制线程池行为:

    25.线程池的拒绝策略有哪些?

    主要有4种拒绝策略:

    1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
    2. CallerRunsPolicy:只用调用者所在的线程来处理任务
    3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
    4. DiscardPolicy:直接丢弃任务,也不抛出异常

    26.说说ThreadLocal的原理

    ThreadLocal 让每个线程,都拥有自己独立的变量副本,线程之间互不干扰,从而实现线程隔离。

    二、底层结构

    1. 每个 Thread 里面,都有一个 ThreadLocalMap 变量
    2. ThreadLocalMap 是一个 Entry 数组,以 ThreadLocal 实例为 key,以 要存的值为 value
    3. 同一个 ThreadLocal,在不同线程里,会读写各自线程的 Map,互不影响

    三、执行流程

    1. 调用 threadLocal.set(value)获取当前线程对象获取线程内部的 ThreadLocalMap以当前 ThreadLocal 为 key,存值
    2. 调用 threadLocal.get()获取当前线程从线程自己的 ThreadLocalMap 里取值
    3. 结果:线程之间完全隔离,不存在共享竞争,天然线程安全。

    四、内存泄漏问题

    1.为什么会泄漏?

    Entry 的 key(ThreadLocal)是弱引用,GC 后 key 会变 nullvalue 是强引用,只要线程不死亡,value 就一直存在

    2.如何避免?

    使用完 手动调用 remove ()尤其在线程池场景,线程复用,必须 remove

    五、使用场景

    • 存储用户上下文、Session、权限信息
    • 数据库连接、事务上下文
    • 线程内需要全局传递,又不想共享的变量

    27.ThreadLocal 和 Synchronized 的区别

  • Synchronized:以时间换空间
  • 利用互斥,让线程排队访问共享变量,同一时间只有一个线程操作,控制并发访问,保证数据安全。

  • ThreadLocal:以空间换时间
  • 线程本地副本,给每个线程分配独立的变量副本不共享变量隔离数据从根本上避免竞争,不需要锁

  • 使用场景
  • Synchronized:多线程共享资源(如计数、库存、订单)。

    ThreadLocal:线程内上下文传递(用户信息、连接、会话)。

    八、JVM

    #面试题目##java面试##java学习[话题]##java#
    JAVA面经实录 文章被收录于专栏

    全网少有的真实 Java 面经合集,持续更新大厂原题、高频考点、手写答案与思路复盘。 无论是校招还是社招,无论是中级还是冲击架构岗,这里都有你面试时真正会被问到的内容。关注不迷路,备战 Java 面试,看这一份就够。

    全部评论

    相关推荐

    评论
    点赞
    收藏
    分享

    创作者周榜

    更多
    牛客网
    牛客网在线编程
    牛客网题解
    牛客企业服务