Java基础常见面试题

基本数据类型

Java中有8种基本数据类型,分别为:

6 种数字类型:

  • 4 种整数型:byte,short,int,long
  • 2 种浮点型:float,double
    1 种字符类型:char
    1 种布尔型:boolean
基本类型 位数 字节 默认值
byte 8 1 0
short 16 2 0
int 32 4 0
long 64 8 0L
char 16 2 ‘u0000’
float 32 4 0.0f
double 64 8 0.0d
boolean 1 1 false

这八种类型对应的包装类型分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean。

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

  1. 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少使用基本数据类型来定义变量,并且,包装类型可以有多个值,而基本类型不可以。
  2. 存储方式:基本数据类型的局部变量存放在 Java 虚拟机的局部变量表中,基本数据类型的成员变量(未做 static 修饰)存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
  3. 占用空间:相比于包装类型(对象类型),基本数据类型占用的空间往往非常小。
  4. 默认值:成员变量是包装类型时默认值是 null,而基本数据类型默认值不是 null
  5. 比较方式:对于基本数据类型来说,== 比较的是值, 对于包装类型来说,== 比较的是对象的内存地址。所有包装类对象之间的比较,都应该使用 equals() 方法。

:基本数据类型存储位置取决于其作用域及生命周期。

包装类的缓存机制?
Byte、Short、Integer、Long 这 4 种包装类默认创建数值 [-128,127] 的相应类型的缓存数据,
Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True 或 False。
如果超出对应范围仍会创建新对象,缓存的范围区间大小只是在性能和资源之间权衡。
两种浮点类型的包装类Float、Double并未实现缓存机制。

Integer缓存源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}

自动装箱与拆箱原理?
什么是自动拆装箱?

  • 装箱:将基本类型用对应的有引用类型包装起来。
  • 拆箱:将包装类型转换为基本数据类型。

装箱就是调用包装类的valueOf()方法,拆箱就是调用xxxValue()方法。

为什么浮点数运算会有精度丢失的风险?
计算机是二进制的,计算机在表示一个数字时,宽度是有限的,无限循环小数存储在计算机中会被截断导致小数精度丢失问题。

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

BigDecimal可以实现对浮点数的运算,不会导致精度丢失。

超过long整型的数字如何表示?

BigInteger内部使用int[]数组来储存,运算效率较低。在使用 BigDecimal 时,为了防止精度丢失,推荐使用它的BigDecimal(String val)构造方法或者 BigDecimal.valueOf(double val) 静态方法来创建对象。

面向对象基础

面向对象三大特征

封装

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。

继承

继承是使用已存在的类的定义作为基础建立新类的技术,新类可以增加新的数据和功能,也可以使用父类的功能,但不能选择性继承。继承可以提高代码的复用性,程序的可维护性。

继承的三大特点:

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问的,只有拥有。
  2. 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  3. 子类可以拥有自己的方法形式定义父类的方法。(以后介绍)

多态

一个对象具有多种状态,具体表现为父类的引用指向子类的实例。
多态的特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型是从其方法调用的返回值类型来决定哪个方法,必须在程序运行时才能确定;
  • 多态不能单用,只在子类存在任意父类方法不存在的时候;
  • 如果子类没有重写父类方法,具体执行的是父类的写的方法,执行的就是父类的。

接口和抽象类异同?

接口和抽象类的共同点

实例化: 接口和抽象类都不能直接实例化对象,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
抽象方法: 接口和抽象类可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。

接口和抽象类的区别

  • 设计目的: 接口主要用于对类的行为进行约束,接口就拥有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。

  • 继承和实现: 一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。一个接口可以继承多个其他接口。

  • 成员变量: 接口中的成员变量只能是 public static final,不能被修改且必须有初始值。抽象类的成员变量可以有任意修饰符,可以在子类被重新定义或赋值。

  • 方法:

    • 在 Java 8 之前,接口中的方法默认是 public abstract,也就是只有方法声明。自 Java 8 起,可以在接口中定义 default(默认)方法和 static(静态)方法。自 Java 9 起,接口也可以包含 private 方法。

    • 抽象类可以包含抽象方法和具体方法。抽象类方法有具体实现,可以直接在抽象类中使用或在子类中实现。非抽象方法具有具体实现。

在 Java 8 以及以后的版本中,接口可以拥有新的方法声明:default 方法、static 方法和 private 方法。这些方法被接口的实现类使用时必须满足一定规则。

深拷贝和浅拷贝的区别?什么是引用拷贝?

  • 浅拷贝: 浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的引用类型的变量,浅拷贝会直接复制内部对象的引用地址,也就是说浅拷贝对象和原对象共用一个内部对象。

  • 深拷贝: 深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

  • 引用拷贝:两个不同的引用指向同一个对象。

Loading

Object类

Object类是一个特殊的类,是所有java类的父类,主要提供以下11种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }

== 与 equals() 的区别?

== 对基本类型与引用类型是不同的:

  • 对基本类型是比较值。

  • 对引用类型是比较对象的地址。

注:java只有值传递,对于 == 而言,不论是对于基本数据类型还是引用类型都是比较值。

equals()不能用于判断基本类型,只能用于判断对象是否相对。

equals()方法存在两种使用方法:

  • 类没有重写equals():等价于 == 。

  • 类重写equals():比较两个对象中属性是否相等。

hashcode() 有什么用?

hashcode的作用是获取对象的hash码。有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)。

为什么重写 equals() 时必须重写 hashCode() 方法?

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

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

String

String、StringBuffer、StringBuilder 的区别?

  • 可变性: String 是不可变的。StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 finalprivate 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。

  • 线程安全:String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilderStringBuilder 与 StringBuffer 的公共父类。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

  • 性能:每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

string 类为什么是不可变的?

  • 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。

  • String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

字符串拼接使用 + 还是 StringBuilder?

Java自身并不支持运算符重载, + 与 += 是专门为 String类重载的,也是Java仅有的重载运算符。

字符串对象通过调用 + 拼接字符串的方式,本质上是通过StringBuilder调用append()方法,拼接完成后调用toString()得到String对象。

String s1 = new String(“abc”);这句话创建了几个字符串对象?

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

  • 字符串常量池中不存在 “abc”:会创建 2 个 字符串对象。一个在字符串常量池中,由 ldc 指令触发创建。一个在堆中,由 new String() 创建,并使用常量池中的 “abc” 进行初始化。

  • 字符串常量池中已存在 “abc”:会创建 1 个 字符串对象。该对象在堆中,由 new String() 创建,并使用常量池中的 “abc” 进行初始化。

String#intern 方法有什么作用?

String.intern() 是一个 native (本地) 方法,用来处理字符串常量池中的字符串对象引用。

  • intern() 方法的主要作用是确保字符串引用在常量池中的唯一性。

  • 当调用 intern() 时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。

Java的反射机制

何为反射?

通过反射可以获取任意一个类的所有属性和方法,还可以调用。

反射的运用场景

Spring/Spring Boot、Mybatis框架都使用到了反射。

JDK动态代理的实现也依赖于反射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;

public DebugInvocationHandler(Object target) {
this.target = target;
}


public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}

Java中的注解也运用到了反射。

反射机制的优缺点

优点:使代码更加灵活,为各种框架提供便利。

缺点:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点。

获取Java对象的方式

  1. 知道具体类的情况:
1
Class alunbarClass = TargetObject.class;
  1. 通过 Class.forName() 方法传入类的全路径获取
1
Class alunbarClass1 = Class.forName("cn.test.TargetObject");
  1. 通过对象实例 Instance.getClass() 获取
1
2
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
  1. 通过类加载器 xxxClassLoader.loadClass() 传入类路径获取:
1
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");

通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行。


Java基础常见面试题
http://bloomivy.github.io/2025/01/19/Java基础常见面试题/
作者
Bloom
发布于
2025年1月19日
许可协议