文章

面试题易错点笔记 | Java 基础面试题

Java 语言

Java 语言有什么特点?

参考回答:

  • Java 是一门面向对象的编程语言,具有封装、继承、多态三大特性:

    • 封装:封装将类的属性和行为封装起来,只提供必要的方法供外部进行访问,以此来保护数据和实现细节,可以有效增强程序的安全性和可维护性

    • 继承:继承允许一个类继承另一个类的成员变量或方法,从而减少重复代码。通过继承,子类可以复用父类的已有功能,还可以在此基础上进行扩展或修改

    • 多态是指允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。通常分为编译时多态和运行时多态,分别通过方法重载(Overload)和重写(Override)实现

      • 编译时多态:指在编译阶段,编译器就能够确定调用哪个方法,这是通过方法的重载来实现的。编译器在编译时根据方法的参数数量、类型或顺序来选择调用合适的方法

      • 运行时多态:在程序运行时,根据实际对象的类型来确定调用的方法,这是通过方法的重写来实现的。运行时多态主要依赖于对象的实际类型,而不是引用类型

  • Java 有跨平台性:关于 Java 有句话叫“Write Once, Run Anywhere”. Java 通过 JVM 虚拟机实现硬件无关以及操作系统无关,已编译的 Java 程序可以在任何有 JVM 的平台上运行

  • Java 具有稳健性:

    • Java 是强类型语言,允许在编译时检查类型不匹配的问题,Java 要求显式的方法声明,它不支持 C 风格的隐式声明。这样保证了编译程序能够捕获到调用错误,提升了程序的可靠性

    • Java 自带异常处理,通过 try/catch/finally 语句简化了异常处理和恢复的过程

Java 和 C++ 的区别

  • C++ 支持多继承,并且 C++ 有指针的概念,由程序员自己来进行内存管理

  • Java 类不支持多继承,但是可以使用接口实现多继承,Java 中不提供指针,而是由 JVM 来管理内存

PS: Java 类不支持多继承解决了 C++ 中多继承时可能出现的菱形问题,即因为同时继承的多个类中有相同的成员变量或方法导致的冲突问题,不过 Java 接口的多继承也有可能发生这种冲突,因此 Java 要求发生冲突时,必须手动对对应的方法进行重写,指定方法的实现方式。

什么是字节码?

JVM 可以理解的代码就是字节码,也就是我们编译完程序后常见到的 .class 文件中存储的内容。字节码不面向某一特定的平台,只面向虚拟机,这就是为什么 Java 程序无需重新编译就可以在不同平台的 JVM 上运行。我们写的 Java 程序,会经历从 .java 经过 javac 命令编译后变成 .class 文件,然后经 JVM 类加载器加载,解释器解释后变成机器语言,在指定的平台上运行。而且对于热点代码,JVM 还引入了 JIT (Just-In-Time)进行运行时编译,以此提升运行效率。这也是为什么经常说 Java 语言“解释和编译并存”

Java 基本语法

移位运算符

Java 里有时会使用移位运算来提升效率,比如 HashMap 的源码中 hash 方法就包含一个 >>>16 的无符号右移 16 位的操作。Java 中有三种移位操作:

  • >> :带符号右移,向右移若干位,高位补符号位,低位丢弃,即正数高位补 0,负数高位补 1。 x >> 1 相当于把 x 除以 2

  • >>> :无符号右移,向右移动若干位,高位补 0,低位丢弃

  • << :左移运算符,向左移动若干位,高位丢弃,低位补 0。在没有溢出的情况下, x << 1 相当于把 x 乘以 2

在 Java 里,事实上移位运算符支持的只有 int 和 long ,编译器对 short 、byte 和 char 进行移位操作前,都会先转换成 int 再进行操作,而 float 和 double 因为在二进制中表现特殊,无法进行移位操作。

另外,当移位运算的位数超过数值的位数时,会先进行取余 % 操作,再进行移位。比如如果对 32 位的 int 变量右移 42 位,实际上是将其右移 10 位。

如下所示:

int a = 100;
System.out.println("初始数据:                      " + a);
System.out.println("初始数据二进制形式:             " + Integer.toBinaryString(a));
a <<= 33;
System.out.println("左移 33 位操作后的数据:         " + a);
System.out.println("左移 33 位操作后的数据二进制形式:" + Integer.toBinaryString(a));
初始数据:                      100
初始数据二进制形式:             1100100
左移 33 位操作后的数据:         200
左移 33 位操作后的数据二进制形式:11001000

final、finally、finalize 的区别

  • final 关键字用于修饰类、方法和变量,表示类不能被继承,方法不能被重写,变量不能被重新赋值

  • finally 关键字用于异常处理语句,通常是 try-catch-finally 的一部分,无论是否发生异常,finally 中的代码块都会被执行

  • finalize 是 Object 类中的一个方法,一般由垃圾收集器来进行调用,但是 JVM 不保证这个方法总是被执行,因此目前通常不会使用这个方法

final 关键字的作用

  • final 关键字修饰的类不能被继承

  • final 关键字修饰的方法不能被重写

  • final 关键字修饰的变量被称为常量,常量必须进行初始化,初始化之后值不能被修改

Java 方法

静态方法为什么不能调用非静态成员变量?

这里需要结合 JVM 的知识,主要原因有两个:

  1. 静态方法是属于类的,在类加载之后就会分配内存,可以通过类名直接进行访问。而非静态成员变量是属于实例对象的,只有在对象实例化之后才存在,需要使用类的实例对象进行访问

  2. 非静态成员变量还不存在时,静态方法已经存在,此时调用内存中还不存在的非静态成员变量显然是非法操作

重载和重写的区别是什么?

前面提到,方法重载实现编译时多态,方法重写实现运行时多态,二者的区别具体如下:

  • 重载:方法重载是同一个类中(或者父类和子类之间)的概念,即对于相同的方法名,有不同的参数类型、参数个数或参数顺序,返回值类型或者访问修饰符可以不同。重载就是对一个类中的同名方法根据传参的不同来执行不同的逻辑

  • 重写:重写是父类和子类之间的概念,子类对父类允许访问的方法重新进行实现

    • 构造方法无法重写

    • 方法名、参数列表必须相同,子类方法返回值应该比父类范围更小或者相等,抛出的异常范围小于等于父类,访问修饰符大于等于父类

    • 如果父类的方法修饰符有 private/final/static ,则无法进行重写,但是 static 修饰的方法可以再次被子类声明

什么是可变长参数?

从 Java 5 开始,Java 支持定义可变长参数,即允许在调用时传入不定长度的参数,例如:

public method(String str1, String ... args){
  //...
}

这个方法中,对于 args 参数,我们可以传入 0 个或者多个参数,需要注意的是,可变长参数只能作为方法的最后一个参数,前面可以有也可以没有任何其他参数

另外,在遇到方法重载的情况时,会优先匹配固定参数。可变长参数实际上在编译时会被转换为一个数组进行操作。

Java 基本数据类型

Java 中有哪几种基本数据类型?

  • 6 种数字类型

    • 4 种整数型

      • byte: 8 位(1 字节),默认值为 0,取值范围是 -128(-2^8) ~ 127(2^8-1)

      • short: 16 位(2 字节),默认值为 0,取值范围是 -32768(-2^{15})~ 32767(2^{15} - 1)

      • int: 32 位(4 字节),默认值为 0,取值范围是 -2147483648(-2^{32}) ~ 2147483647(2^{32}-1)

      • long: 64 位(8 字节),默认值为 0L,取值范围是 -9223372036854775808(-2^{63}) ~ 9223372036854775807(2^{63} -1)

    • 2 种浮点型

      • float: 32 位(4 字节),默认值为 0f,取值范围是 1.4E-45 ~ 3.4028235E38

      • double: 64 位(8 字节),默认值为 0d,取值范围是 4.9E-324 ~ 1.7976931348623157E308

  • 1 种字符类型

    • char: 16 位(2 字节),默认值为 'u0000',取值范围是 0 到 65535 (2^{16} - 1)

  • 1 种布尔型

    • boolean: 1 位(未明确指定字节数,通常实现为 1 字节或更少),默认值为 false,取值范围是 true 或 false。

包装类型的缓存机制

除了 Float 和 Double 以外,基本数据类型的包装类型都使用了缓存机制来提升性能

其中, Char 的范围是 0 到 127,其它数值类型的范围都是 -128 到 127,Boolean 的范围是 true 和 false

当使用 new 关键字创建包装类型时,会创建一个新的对象,而如果使用其他方式,则会考虑使用缓存,因此对于包装类型,我们通常是使用 equals 方法来对包装类型的值进行比较,避免出错

如何解决浮点数运算精度丢失的问题?

我们知道,在计算机中,用二进制来存储数据,而对于无限循环的二进制小数,我们通常只能截断出一部分来进行存储,截断掉的部分,就是丢失的精度。

对此,Java 提供了一个 BigDecimal 类,凡是涉及到金额等对浮点数运算精度有要求的场景,我们都应当使用这个类来进行浮点数运算,以此保证计算精度

面向对象基础

接口和抽象类有什么异同?

共同点:

  • 都不能被实例化

  • 都可以包含抽象方法

  • 都可以有默认实现的方法

区别:

  • 一个类只能继承一个类,但可以实现多个接口

  • 接口中的成员变量 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认是 default ,可以在子类中被重新定义或者赋值

  • 接口主要用于对类的行为进行约束,而抽象类主要的作用是用于代码复用

什么是深拷贝和浅拷贝?

浅拷贝会在堆上创建一个新的对象,但是如果原对象中包含引用类型的成员变量,新的对象中对应的成员变量会指向与原对象成员变量相同的地址,即浅拷贝只完成了对自身的拷贝,而对自身的引用类型的成员变量没有进行拷贝操作。

深拷贝则是会完全复制整个对象,包括这个对象包含的内部对象

实现浅拷贝的方法:实现 Cloneable 接口,重写 clone 方法即可,仅需在新的实现里通过 super.clone() 去调用父类 Object 类的 clone 方法就行了,需要注意显式类型转换。

实现深拷贝:和实现浅拷贝类似,但是需要让内部对象所属的类也实现 Cloneable 接口,重写 clone 方法,同时,在自身的 clone 方法中调用内部对象的 clone 方法,将拷贝出的新的内部对象赋值给新的深拷贝出来的对象即可。

还有一个更加有意思的思路,是使用序列化和反序列化,只需要对一个对象进行序列化和反序列化,新的反序列化出来的对象就是一个与原对象具有相同属性的深拷贝的对象

Java 中是值传递还是引用传递?

Java 里只有值传递,对于基本数据类型,传递的是类型自身的值,对于引用类型,传递的是引用的值,也就是对象的地址

Java 常见类

Object

== 和 equals() 的区别是什么?

对于基本数据类型, == 比较的是值,对于对象,== 比较的是内存地址

而 equals() 通常规定用来比较两个对象的值,不过 equals() 在 Object 类中默认的实现是比较内存地址,因此通常我们需要重写对象的 equals() 方法

hashCode() 方法有什么用?

hashCode() 方法是定义在 Object 类中被 native 修饰的本地方法,作用是获取对象的哈希值,用于散列存储。

注意:hashCode() 和 equals() 在散列的集合容器中存在以下关系:

  • 哈希值不等时,两个元素必定不等

  • 哈希值相等时,两个元素不一定相等,还需要再使用 equals() 方法进行判断(因为可能存在哈希碰撞)

为什么重写 equals() 方法后还需要重写 hashCode() 方法?

参考前一道题,我们需要保证使用 equals() 判断两个对象相同时,他们的哈希值也是一样的

String

String、StringBuilder、StringBuffer 的区别是什么?

首先,从可变性来说,String因为是使用 final关键字对 value数组进行修饰,所以它是不可变的,而 StringBuilder 和 StringBuffer 都继承自 AbstractStringBuilder 类,在这个类中,也使用数组保存字符串内容,但是没有用 final关键字修饰,更关键的是,这个类还提供了修改字符串的方法,比如 append、reverse等,所以这两个是可变的。

然后从线程安全的角度说,String 类型的对象是不可变的,也可以理解为常量,所以是线程安全的。StringBuffer 对各个方法加了同步锁(synchronized 关键字),所以也是线程安全的,而 StringBuilder 中则没有这些机制,是线程不安全的。

性能上来说,对 String 类型想进行修改,实际上就是创建一个新的 String 对象,然后把指针指向这个对象。StringBuffer 和 StringBuilder 则不需要生成新的对象,而且 StringBuilder 中因为没有加锁,单线程情况下会比 StringBuffer 性能更高。

如果只是操作少量的数据,可以使用 String,单线程操作大量数据,适用 StringBuilder,多线程存在竞争的情况下操作大量数据,则适合使用 StringBuffer

String 为什么是不可变的?

  1. 保证字符串内数组被 final 修饰且是私有的,并且 String 没有提供修改这个字符串的方法

  2. 用 final 修饰类,以此避免子类破坏 String 的不可变性

String str = new String("abc") 会创建几个对象?

会创建 1 个或 2 个字符串对象:

  • 如果字符串常量池中已有 "abc" 对象,就会在堆上创建一个字符串对象,并将 str 指向这个对象

  • 如果字符串常量池中没有 "abc" 对象,就会在堆上创建两个字符串对象,然后一个引用保存在常量池中,一个被 str 引用

异常

Error 和 Exception 的区别是什么?

  • Error:JVM 无法解决的问题,如栈溢出、内存溢出等程序无法解决的问题

  • Exception:一般性问题,可以在代码中解决,比如空指针等

try - catch - finally 中 finally 一定会执行吗?

如果程序执行过对应的 try 代码块,则这个 finally 一定会执行。有两种情况 finally 不会执行:

  1. 未执行到对应的 try 代码块

  2. 线程执行到对应的 try 或者 catch 代码块时被终止,极端情况如断电或死机

反射

什么是反射?

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。

反射常见应用场景

  • 动态代理

  • 注解

动态代理的几种方式

常见两种动态代理方式:JDK 动态代理机制和 CGLIB 动态代理机制。

JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类,是通过继承实现的。

从效率来说,大部分情况下 JDK 动态代理更加优秀

IO

什么是 BIO / NIO / AIO

  • BIO(Blocking IO)同步阻塞 IO,服务器实现模式为一个连接一个线程,就是客户端有连接请求的时候,服务器就需要启动一个线程去进行处理,如果连接不做任何事情就会造成不必要的线程开销,可以使用线程池机制进行优化

  • NIO(NEW IO)同步非阻塞 IO,服务器实现模式是一个请求一个线程,就是客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 IO 请求时才启动一个线程进行处理。适用于连接数目多且连接比较短的架构,比如聊天服务器

  • AIO(Asynchronous IO)异步非阻塞 IO,服务器实现模式是一个有效请求一个线程,也就是客户端的 IO 请求都是由操作系统先完成了在通知服务器启动线程进行处理。适用于连接数多且连接比较长的架构,比如相册服务器,充分调用 OS 参与并发操作

什么是同步和异步?

  • 同步:调用者需要一直主动等待被调用者的结果

  • 异步:调用者调用被调用者后,不会立刻得到结果,调用者发起调用后,被调用者通过状态、通知或者回调函数等方式让调用者得到结果

阻塞和非阻塞的区别是什么?

阻塞和非阻塞关注的是线程的状态

  • 阻塞调用指的是调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会恢复运行

  • 非阻塞调用则是指在不能立刻得到结果之前,该调用不会阻塞当前线程

希望这篇分享能为你带来启发!如果你有任何问题或建议,欢迎在评论区留言,与我共同交流探讨。

License:  CC BY 4.0