Java最全八股文速通

一、Java基础

1、JVM vs JRE vsJDK

  • JVM是Java程序运行的虚拟机,负责将字节码转换成机器码并执行。
  • JRE是Java程序的运行环境,包含了JVM和必要的类库,用于运行已经编译好的Java程序。
  • JDK是Java开发工具包,包含了JRE和开发工具,用于开发Java程序。

这三者之间的关系是层层嵌套的:JDK > JRE > JVM。在开发Java程序时,需要安装JDK;而在运行Java程序时,只需要安装JRE即可。

2、Java 中的几种基本数据类型

  • 4 种整数型:byte(1字节)、short(2字节)、int(4字节)、long(8字节)
  • 2 种浮点型:float(4字节)、double(8字节)
  • 1 种字符类型:char(2字节)
  • 1 种布尔型:boolean(特殊,1字节,但具体实现可能依赖于虚拟机)

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

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

4、包装类型的缓存机制
java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。

  • Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据
  • Character 创建了数值在 [0,127] 范围的缓存数据
  • Boolean 直接返回 True or False
  • 两种浮点数类型的包装类 Float,Double 并没有实现缓存机制

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。两种浮点数类型的包装类

5、自动装箱与拆箱是什么及其原理?

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

装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。因此,Integer i = 10 (自动装箱)等价于 Integer i = Integer.valueOf(10)。int n = i(自动拆箱) 等价于 int n = i.intValue();

6、为什么浮点数运算的时候会有精度丢失的风险?如何解决?
为什么:这和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
解决:BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的
浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断,这都是因为会出现精度问题。所以浮点数比较有两种比较方法。
第一种方法指定一个误差范围,如Math.abs(a-b)<1e-6F。
第二种方法是用BigDecimal的compareTo去比较,compareTo() 方法比较的时候会忽略精度。

7、成员变量与局部变量的区别?

  • 定义位置:成员变量是在类中定义的,但在方法外部。它们是类的一部分,并且可以被类的所有方法访问。局部变量是在方法、构造函数或代码块内部定义的。它们的作用范围仅限于定义它们的代码块内
  • 作用范围:成员变量作用范围是整个类。它们可以在类的任何方法、构造函数或内部类中访问(前提是访问权限允许)。局部变量作用范围仅限于定义它们的方法、构造函数或代码块。方法外部无法访问局部变量
  • 生命周期:成员变量生命周期与对象的生命周期相同。它们在对象创建时被分配内存,直到对象被垃圾回收时才释放内存。局部变量生命周期仅限于定义它们的代码块执行期间。当代码块执行完毕后,局部变量的内存会被释放。
  • 默认值:成员变量有默认值。如果没有显式初始化,它们会被自动初始化为默认值。局部变量没有默认值。在使用之前必须显式初始化,否则编译器会报错。
  • 访问权限:成员变量可以使用访问修饰符(如 public、private、protected、默认)来控制访问权限。局部变量没有访问修饰符。它们的作用范围完全受限于定义它们的代码块。

8、重载和重写有什么区别?

  • 定义:重载指在同一个类中定义多个方法,它们的名称相同,但参数列表不同(参数的数量、类型或顺序不同)。
    返回类型可以相同也可以不同,但仅凭返回类型不能区分重载的方法,重写指子类重新定义从父类继承的方法,方法名称、参数列表、返回类型都必须与父类中的方法完全相同。
  • 目的:重载主要用于增加方法的灵活性,使得一个方法名称可以用于不同的参数类型和数量。可以使代码更具可读性和简洁性。重写主要用于在子类中提供父类方法的特定实现。允许子类根据需要修改或扩展父类的行为,从而实现多态性。
  • 访问修饰符:重载访问修饰符可以不同。即使方法具有相同的名称和不同的参数,访问修饰符的不同也不会影响重载。重写访问修饰符不能减少父类方法的访问权限。即子类重写的方法访问修饰符必须与父类中相应的方法的访问修饰符相同或更宽松
  • 异常处理:在重载方法中,异常类型和数量可以不同。重载方法的异常处理不受限制。在重写方法中,子类方法可以抛出比父类方法更少或相同的异常,但不能抛出更多或不同的异常。这是为了保持父类和子类的一致性。
  • 发生阶段:重载方法发生在编译期,重写方法发生在运行期。

9、什么是可变长参数?

  • 从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。可以接受 0
    个或者多个参数。(String… args)
  • 可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
  • 遇到方法重载的情况会优先匹配固定参数而不是可变参数
  • Java 的可变参数编译后实际会被转换成一个数组

10、面向对象和面向过程的区别

  • 面向过程编程(POP):面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
  • 面向对象编程(OOP):面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。

相比较于 POP,OOP 开发的程序一般具有下面这些优点:

  • 易维护:由于良好的结构和封装性,OOP 程序通常更容易维护。
  • 易复用:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能
  • 易扩展:模块化设计使得系统扩展变得更加容易和灵活。

11、Java 创建对象有几种方式

  • new关键字:这是最常见的创建对象的方式。通过调用类的构造函数,使用new关键字可以在内存中分配一个新的对象。
Person person=new Person();
  • 反射:Java的反射机制允许在运行时动态地创建对象。通过获取类的Class对象,并调用其构造函数,可以实现对象的创建。
Class clazz=Class.forName("Person");
Person person=(Person)clazz.getDeclaredConstructor().newInstance();
  • clone()方法:如果类实现了Cloneable接口,并重写 clone() 方法:就可以使用clone()方法创建对象的副本。
Person person2 = (Person) person1.clone();
  • 对象的反序列化:通过将对象序列化到一个字节流中,然后再进行反序列化,可以创建对象的副本,。
FileInputStream fileInputStream = new FileInputStream("person.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Person person = (Person) objectInputStream.readObject();

其中,使用new关键字是最常见和推荐的创建对象的方式。其他方式通常在特定场景下使用,如需要动态创建对象或创建对象的副本等情况。

12、构造方法有哪些特点?是否可被 override?

  • 名称与类名相同:构造方法的名称必须与类名完全一致。
  • 没有返回值:构造方法没有返回类型,且不能使用 void 声明。
  • 自动执行:在生成类的对象时,构造方法会自动执行,无需显式调用。

构造方法不能被重写(override),但可以被重载(overload)。因此,一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。

13、面向对象三大特征

  • 封装是指将对象的状态(属性)和行为(方法)结合在一起,并将这些实现细节隐藏在对象内部,只暴露出接口给外部使用。通过封装,对象的内部状态可以被保护,不被外部直接访问,从而减少系统复杂性,提高安全性和灵活性。
  • 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,从而实现代码重用。子类可以继承父类的公共和保护成员,并可以对其进行扩展和修改。
  • 多态是指不同的对象对同一消息做出不同的响应。多态有两种主要形式:方法重载(编译时多态)和方法重写(运行时多态)。

14、接口和抽象类有什么共同点和区别?
接口和抽象类的共同点:

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

接口和抽象类的区别

  • 设计目的:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 继承和实现:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。
  • 成员变量:接口中的成员变量只能是 public static final类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(private, protected,public),可以在子类中被重新定义或赋值。
  • 方法: Java 8 之前,接口中的方法默认是 public abstract ,也就是只能有方法声明。自 Java 8起,可以在接口中定义 default(默认) 方法和 static (静态)方法。 自 Java 9 起,接口可以包含 private方法。抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须在子类中实现。非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写。

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

  • 浅拷贝:浅拷贝创建一个新的对象,但并不复制对象所引用的内部对象。浅拷贝只复制对象的基本属性值和对象的引用。这意味着,如果对象包含对其他对象的引用,那么这些引用仍然指向原来的对象,而不是创建新的副本。
  • 深拷贝:深拷贝创建一个新的对象,并递归地复制对象内部所引用的所有对象。这样,新的对象和原始对象完全独立,没有共享的引用。深拷贝确保所有的内部对象也被复制,避免了共享引用的问题。
  • 引用拷贝 引用拷贝并不真正复制对象,而是创建一个新的引用指向原来的对象。换句话说,两个引用变量指向同一个对象。引用拷贝在内存中不会创建新的对象实例,只是复制了对象的引用。
    在这里插入图片描述

16、Object 类的常见方法有哪些?

//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 { }

17、equals与 = = == == 区别
在Java中, = = == ==是一个比较操作符,用于比较两个变量的值是否相等。而"equals()"是Object类中定义的方法,用于比较两个对象是否相等
具体区别如下:

  • = = == ==用于比较基本数据类型和引用类型变量的地址值是否相等。对于基本数据类型,比较的是它们的实际值;对于引用类型,比较的是它们所引用的对象的地址值。
  • equals()方法用于比较两个对象的内容是否相等。默认情况下,它与 = = == ==的作用相同,比较的是对象的地址值。但是,可以根据具体的类重写该方法,以实现自定义的比较逻辑,String已经重写了equals方法。

需要注意以下几点:

  • 对于基本数据类型,使用 = = == ==进行比较更加直接和高效
  • 对于引用类型,使用equals()进行比较更加准确和灵活,但需要注意重写"equals()“方法,以满足自定义的比较需求。
    总结起来,”=="比较的是变量的值或引用的地址值,而"equals()"比较的是对象的内容
String a=new String("123");
String b=new String("123");
System.out.println(a==b);   //false
System.out.println(a.equals(b));  //true

18、hashCode()有什么用
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。虽然,每个Java类都包含hashCode() 函数,但只有在Java集合中本质是散列表的类才有用,如HashMap,Hashtable,HashSet。
例如:当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

19、为什么重写 equals() 时必须重写 hashCode() 方法?
hashCode() 和 equals() 方法之间有一个重要的契约(约定)。这个契约规定了,如果两个对象通过 equals() 方法比较认为是相等的(即 equals() 返回 true),那么这两个对象的 hashCode() 方法必须返回相同的哈希码。这个契约是确保哈希表(如 HashMap、HashSet)等哈希数据结构正常工作的基础。

20、String、StringBuffer、StringBuilder 的区别?

  • String:不可变,线程安全,适合不需要修改的字符串。
  • StringBuffer:可变,线程安全,适合多线程环境中频繁修改的字符串。
  • StringBuilder:可变,不线程安全,适合单线程环境中频繁修改的字符串。

21、字符串拼接用“+” 还是 StringBuilder?
字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。

22、字符串常量池的作用
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

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

  • 如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中
  • 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”

24、String.intern 方法有什么作用?
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

  • 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。

  • 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。

// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
System.out.println(s1 == s2); // true
System.out.println(s3 == s4); // false
System.out.println(s1 == s4); //true

25、String 类型的变量和常量做“+”运算时发生了什么?
String 类型的变量和常量使用 + 运算符时,实际上是进行字符串的连接操作。具体来说:

  • 编译时常量:如果 + 运算符用于连接编译时常量(即编译时确定的字符串字面量),编译器会在编译时就把它们合并成一个新的字符串。这意味着在代码运行时,这些常量已经被合并为一个单独的字符串了。例如:
String str = "Hello, " + "World!";
// 编译后,str 实际上是 "Hello, World!"
  • 运行时拼接:如果 + 运算符涉及到的不是全部编译时常量,在运行时会使用 StringBuilder来执行拼接操作。StringBuilder 是一个可变的字符序列,它在拼接字符串时会更高效。示例如下:
String a = "Hello, ";
String b = "World!";
String result = a + b; // 运行时拼接,使用 StringBuilder

在这个例子中,a 和 b 是变量,Java 会在运行时使用 StringBuilder 来进行连接操作,然后再将 StringBuilder 的内容转为 String 对象。
对于频繁进行字符串拼接的场景,特别是涉及到大量字符串拼接的情况,使用 StringBuilder 显得更加高效。因为每次使用 + 运算符进行字符串拼接时,如果不使用 StringBuilder,每次拼接都会生成新的 String 对象,增加了内存的消耗和垃圾回收的负担。

二、Java集合

在这里插入图片描述

2.1、List集合

1、ArrayList和LinkedList有什么区别
ArrayList和LinkedList是)java集合框架中List接口的两个常见实现类,它们在底层实现和性能特点上有以下几点区别:
1).底层数据结构:ArrayList使用动态数组来存储元素,而LinkedList使用双向链表来存储元素。
2).随机访向性能:Arraylist支持高效的随机访问,因为它可以通过下标计算元素在数组中的位置。而Linkedlst在随机访问方面性能较差,获取元素需要从头或尾部开始遍历链表找到对应位置。
3)、插入和删除性能:ArayList在尾部添加或删除元素的性能较好,因为它不涉及数组的移动。而在中间插入或删除元素时,ArrayList涉及到元素的移动,性能相对较低。LinkedList在任意位置进行插入和删除操作的性能较好,因为只需要调整链表中的指针即可。
4).内存占用:ArrayList底层是数组,内存连续,节省内存,而LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
5)、扩展性:ArrayList 在元素超过当前容量时,需要创建一个更大的数组并复制原有元素,可能会造成性能开销。LinkedList 可以方便地在任意位置插入元素,不需要移动其他元素。
综上所述,当频繁访问元素时,使用 ArrayList 更高效,当频繁插入和删除操作时,使用 LinkedList 更合适。
2、ArrayList底层的实现原理是什么
1)、底层数据结构:ArrayList底层是用动态的数组实现的
2)初始容量:ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
3)扩容逻辑:ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
4)添加逻辑:确保数组已使用长度(size)加1之后足够存下下一个数据。计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)。确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。返回添加成功布尔值

2.2、Map集合

1、HashMap底层的实现原理是什么
HashMap的数据结构: 底层使用hash表数据结构,即数组和链表或红黑树
1). 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
2). 存储时,如果出现hash值相同的key,此时有两种情况。a. 如果key相同,则覆盖原始值;b. 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
3) 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
4)JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表

三、Java并发编程

四、Java虚拟机

五、Spring

六、SpringBoot

七、Mybatis

八、SpringCloud

九、Mysql

十、Redis

十一、消息中间件RabbitMQ,Kafka

十二、设计模式

十三、Linux

十四、ElasticSearch

十五、场景题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值