【面试八股文】java基础知识

引言

本文是java面试时的一些常见知识点总结归纳和一些拓展,笔者在学习这些内容时,特地整理记录下来,以供大家学习共勉。

一、数据类型

1.1 为什么要设计封装类,Integer和int区别是什么?

使用封装类的目的

  • 对象化: 基本数据类型如int、double不是对象,无法直接利用面向对象的特性,如方法扩展、继承等。封装类将基本类型转化为对象,允许它们具有方法,可以更容易地操作和传递。

  • 空值处理: 基本数据类型必须有值,而封装类可以为null,这在某些情况下非常有用,比如数据库查询结果可能为null时。

  • 自动装箱与拆箱: Java提供了自动装箱和拆箱功能,使得基本类型和封装类之间可以无缝转换,方便编程。

  • 集合框架兼容性: Java集合框架(如List、Set、Map)不能直接存储基本类型,只能存储对象。封装类使得基本类型能够被放入集合中。

Integer与int的区别

  • 类型: int是Java的一个基本数据类型,而Integer是int的封装类,属于对象。

  • 内存分配: int变量直接存储在栈上,空间小且效率高;Integer对象存储在堆上,需要额外的空间用于存储对象信息,并且创建和回收对象有一定的开销。

  • 初始化默认值: int类型的默认值为0;Integer类型的默认值为null,表示未指向任何对象。

  • 可变性: int一旦赋值后不可更改;而Integer作为对象,虽然其内部的值通常视为不变,但作为对象本身是可以被替换的。

  • 自动装箱与拆箱: Java会自动在int和Integer之间转换,这个过程称为自动装箱和拆箱。例如,可以直接将int值放入需要Integer的集合中,反之亦然。

1.2 Integer类型下为什么"1000 == 1000"是false,"100 == 100" 是true?

以下是代码:

java 复制代码
    public static void main(String[] args) {
        Integer a = 100, b = 100,c = 1000,d = 1000;
        System.out.println("100 == 100 :"+(a == b));
        System.out.println("1000 == 1000 :"+(c == d));
    }

运行结果如下:

为什么呢?这要分析Integer的源码了

Integer中有个方法valueOf(),从源码可以看到,当i值处于一定范围时,返回的其实是IntegerCache缓存里的值,并没有new Integer()对象,当不在这个范围时,返回新对象。

再看下面的代码,IntegerCache是一个内部类

IntegerCache.low = -128,IntegerCache.hign = 127,其实就是-128~127之间的数都缓存到IntegerCache.cache数组里面了。

Integer a = 100,b = 100,100位于-128~127之间。a和b都是从IntegerCache.cache数组中取得同一个对象,所以a == b是true,即 100 == 100 是true。

而1000不在-128~127之间,Integer c = 1000,d == 1000,c和d是创建的两个不同的对象,因此c == d是false,即1000 == 1000是false。

1.3 new String("hello")创建了几个对象?

这是一个很经典的面试题目了,主要考察字符串和字符串常量池的掌握深度。

我们一般声明并赋值一个字符串常量,表达式如下:

java 复制代码
String a = "hello"

这种方式首先会在字符串常量池中查找是否已经存在值为 "hello" 的字符串。如果不存在,会在池中创建一个新对象;如果存在,则直接引用这个已存在的对象。这种方式更高效且有助于节省内存。当常量池中已经存在字符串常量时就不需要创建新的实例了。

再看另外一种创建字符串的方式

java 复制代码
String b = new String("hello")

当使用 String b = new String("hello") 这种方式创建字符串时,具体过程如下:

  • 检查常量池:首先,Java 虚拟机(JVM)会检查字符串常量池中是否已经存在值为 "hello" 的字符串。如果不存在,JVM 会在常量池中创建一个字符串对象,存储内容为 "hello"。
  • 堆上创建对象:不论常量池中是否已有 "hello",都会在堆(heap)上创建一个新的 String 实例。这个新创建的对象包含对常量池中对应字符串的引用(如果之前步骤在池中创建了字符串的话)。变量 b 最终会指向堆上这个新创建的 String 对象。

也就是说 new String("hello")这种方式至少在堆中会创建一个new String()实例,至于会不会先在常量池中创建字符串常量,需要看字符串是否存在于常量池。

1.4 String、StringBuilder、StringBuffer的区别是啥?

关于这三个常用操作字符串的类,可以从以下四个方面来比较:

  1. 值可变性方面

    String内部是final修饰的,它是不可变类。每次对一个字符串的修改操作都会产生一个新的字符串对象。StringBuilder、StringBuffer是可变类,一般用于字符串的拼接操作,字符串变更时不会产生新对象,都是在原有的字符串基础上变化。

  2. 线程安全

    • String 是不可变类,其内部的许多方法都不会对原始字符串做修改,而是返回新字符串对象,即使多个线程访问这个字符串也不用担心修改问题,所以它是线程安全的。
    • StringBuffer是线程安全的,因为它的每个方法几乎都加了synchronized关键字,而且它也是final修饰的不可变类。
    • StringBuilder不是线程安全的。因为其内部没有同步机制,多线程环境下使用StringBuffer操作字符串,单线程下才使用StringBuilder
  3. 性能方面

    String的性能最低,因为其内部不可变性,总是生成新对象,分配内存。然后是StringBuffer,因为它的可变性使得字符串可以直接修改,不需要创建新对象。但是StringBuilder性能是最高的,因为内部没有加锁的同步机制,性能自然最高。

  4. 数据存储

    String通常存储在字符串常量池中,而StringBuffer和StringBuilder存储在堆内存中。

二、Object对象

2.1 如何理解java对象的创建过程?

  1. 声明引用变量:首先,在代码中声明一个引用变量,该变量的类型指定为某个类或接口。此时,变量还没有关联到实际的对象。
java 复制代码
Student student;
  1. 分配内存:当使用 new 操作符实例化一个对象时,Java 虚拟机 (JVM) 会执行以下操作:
  • 计算大小:JVM 计算该对象及其内部成员变量所需的空间。
  • 分配空间:在堆内存中找到足够的连续空间来存储对象。如果内存不足,会触发垃圾回收器尝试释放空间。
  • 初始化零值:为对象的所有成员变量分配默认值,如 int 为 0,boolean 为 false,引用类型为 null。
  1. 初始化
  • 执行构造函数:JVM 调用对应的构造函数来初始化对象。构造函数可以设置成员变量的初始值,执行其他必要的初始化操作。
  • 父类构造函数:如果构造函数中没有显式调用超类构造函数,编译器会自动插入对超类无参构造函数的调用。如果有参数传递给 super(),则按照指定参数调用。
  1. 关联引用:构造函数执行完毕后,新创建的对象的地址被赋给之前声明的引用变量。这时,引用变量才真正"指向"了堆内存中的对象实例。
java 复制代码
   student = new Student();
  1. 对象可达性分析:新创建的对象如果被任何变量引用,那么它就是可达的,不会被垃圾回收。否则,如果没有任何引用指向它,它将成为垃圾回收的候选对象。

2.2 深克隆和浅克隆?

深克隆和浅克隆是Java中对象复制的两种方式,它们主要区别在于对对象引用类型的处理上:

  • 浅克隆(Shallow Clone)
  1. 定义:浅克隆仅复制对象的基本数据类型属性值和引用类型的引用地址。这意味着原始对象和克隆对象将共享引用类型的对象。如果修改其中一个对象中的引用类型属性,另一个对象中的对应属性也会受到影响。

  2. 实现:通过实现 Cloneable 接口并重写 clone() 方法来实现浅克隆。默认的 clone() 方法执行的是浅复制。

  3. 特点:

    • 基本数据类型和 String 类型的属性会被完整复制。
    • 引用类型的属性仅复制引用地址,不复制引用的对象本身。
  • 深克隆(Deep Clone)
  1. 定义:深克隆不仅复制对象本身,还递归地复制对象内部的所有引用类型属性,创建这些引用对象的新实例,确保原始对象和克隆对象之间完全独立,修改一个对象不会影响到另一个对象。

  2. 实现:实现深克隆通常需要手动编写逻辑,或者利用序列化和反序列化的方式来完成。手动实现时,需要对每个引用类型的属性也进行克隆操作,如果是复杂对象,则需递归地进行深克隆。

  3. 特点:

    • 无论基本数据类型、String 类型还是引用类型,都会创建全新的副本。
    • 引用类型的属性会被完全复制,包括它们指向的对象,从而实现完全独立的复制。
  • 总结
    • 浅克隆速度快,因为它只是复制了对象的引用,但可能导致数据一致性问题。
    • 深克隆更耗时,因为它复制了整个对象树,但提供了对象的完全隔离,适用于需要完全独立副本的场景。

2.3 强引用、弱引用、软引用、虚引用的区别?

对于这四种引用类型,我们实际开发中好像并不太去关注,而且实际开发中我们大部分使用的都是强引用。下面逐一介绍,并通过代码示例深入说明。

  1. 强引用 (Strong Reference)
    • 定义:最常见的引用类型,如 Object obj = new Object(); 中的 obj 就是一个强引用。只要强引用存在,垃圾收集器永远不会回收被引用的对象,即使在内存不足的情况下,JVM也会宁愿抛出OutOfMemoryError错误,也不会回收这样的对象。
    • 用途:用于维护程序的关键对象,这些对象在程序逻辑中通常是必须存在的。

下面是一个代码示例

java 复制代码
package com.execute.batch.executebatch;

public class StrongReference {
    public static void main(String[] args) {
        new StrongReference().method1();
    }

    public void method1() {
        Object object = new Object();
        Object[] objArr = new Object[Integer.MAX_VALUE];
    }
}

报错如下:

当运行至Object[] objArr = new Object[Integer.MAX_VALUE]时,如果内存不足,JVM会抛出OOM错误也不会回收object指向的对象。不过要注意的是,当method1运行完之后,object和objArr都已经不存在了,所以它们指向的对象都会被JVM回收。

只要还有强引用指向一个对象,垃圾收集器就不会回收这个对象。当然如果你显示的把引用object 设置 为 null,或者超出对象的生命周期(比如方法调用结束),此时就可以回收这个对象。具体回收时机还是要看垃圾收集策略。

  1. 软引用(SoftReference)

    • 定义:使用 java.lang.ref.SoftReference 类来创建。软引用指向的对象在内存充足时不会被垃圾回收器回收,但在内存不足时将被回收,因此软引用主要用于实现一些内存敏感的缓存。

    • 用途:适合用于构建可牺牲的缓存,如图片缓存、文档缓存等,可以在内存紧张时自动释放以避免OutOfMemoryError。

java 复制代码
package com.execute.batch.executebatch;

import java.lang.ref.SoftReference;
import java.util.Objects;

public class SoftRef {

    public static void main(String[] args){
        System.out.println("start");
        Obj obj = new Obj();
        SoftReference<Obj> sr = new SoftReference<>(obj);
        obj = null;
        System.out.println(Objects.requireNonNull(sr.get()).obj.length);
        System.out.println("end");
    }
}

class Obj{
    int[] obj ;
    public Obj(){
        obj = new int[1000];
    }
}

先创建了一个强引用,在内存充足时,把这个强引用放置到软引用中,接着把强引用置为null,被gc回收了。我们取数据就可以从软引用中获取,速度更快。当然内存不足时,软引用中就会被gc回收。

被软引用关联对象的回收主要看内存是否充足,充足就不回收,不充足就回收,在提升性能和效率的同时,兼顾了资源释放

  1. 弱引用
    • 定义:通过 java.lang.ref.WeakReference 类创建。弱引用的对象无论内存是否足够,只要发生垃圾回收,都会被回收。

    • 用途:适用于非必须的对象,比如映射表中的键,这样当键不再有强引用指向时,垃圾回收器可以自动清理映射表中的条目,避免内存泄漏。

java 复制代码
package com.execute.batch.executebatch;

import java.lang.ref.WeakReference;

public class WeakRef {
    public static void main(String[] args) {
        WeakReference<String> sr = new WeakReference<>(new String("hello"));
        System.out.println(sr.get());
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println(sr.get());
    }
}

代码中手动通知了jvm进行垃圾回收,即弱引用关联的对象,在gc回收时是一定被回收的。

有个注意点: 代码中把new String("hello")改为"hello",就不回收了,都会打印出hello

为什么呢?解释如下:

代码从 new String("hello") 改为 "hello" 时,实际上是在直接引用字符串常量池中的那个唯一的"hello"实例,而不是像之前那样创建一个新的String对象。

  1. 使用new String("hello")时: 这会创建两个String对象:一个是常量池中的"hello",另一个是堆上通过new操作新生成的对象。由于这个新对象只被WeakReference引用,所以在执行System.gc()后,这个堆上的对象可能被垃圾回收器回收,因为WeakReference不会阻止其引用的对象被回收。

  2. 使用"hello"时: 此时直接引用常量池中的字符串,没有额外的堆对象创建。字符串常量池中的对象不会被垃圾回收,因为它们的生命周期与应用程序的运行期相同,并且存在对它们的隐式强引用(即类加载器和字符串字面量的引用)。因此,即使在调用System.gc()之后,通过sr.get()获取到的引用仍然有效,因为它指向的是不可被回收的常量池中的字符串。

  3. 虚引用

等同于没有引用,对象被回收时会收到通知。虚引用不会决定对象的生命周期,它提供一种确保对象被"finalize"以后去做某些事情的机制。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入与之关联的引用队列中,程序可以通过判断引用队列是否已经加入了虚引用来决定被引用对象是否要被垃圾回收器回收。然后我们就可以在引用对象回收前执行一些必要的操作**。所以虚引用必须和引用队列一起使用**。

java 复制代码
package com.execute.batch.executebatch;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceExample {

    public static void main(String[] args) {
        Object referent = new Object();
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        PhantomReference<Object> phantomRef = new PhantomReference<>(referent, queue);

        // 将referent设置为null,使其仅被虚引用引用
        referent = null;

        // 触发垃圾回收,但注意我们无法直接控制GC何时运行
        System.gc();

        // 检查引用队列,看是否已经回收了referent对象
        while (queue.poll() != null) {
            System.out.println("对象被回收了,可以从队列中获取通知");
            referent = null; // 队列处理后重置referent,确保逻辑正确
        }

        // 注意:这里只是演示逻辑,实际回收发生和队列通知的时间取决于JVM的GC行为
    }
}

代码中垃圾回收触发后,对象不是立刻被回收,具体要看gc自己的回收时间。

2.4 一个空的Object对象占多大内存?

对象是存储在堆内存中的,那么一个对象在虚拟机中的内存布局是什么样的呢?

  1. 对象头(Header): 包含了对象的元数据,如对象的哈希码、GC分代年龄、锁状态标志、类型指针等。这部分的大小依赖于具体的JVM实现和操作系统。在64位的HotSpot JVM中,对象头通常占用12或16字节,具体取决于是否启用了压缩指针。

  2. 实例数据(Instance Data): 对于一个没有任何实例变量的空Object,这部分实际上是0字节。

  3. 对齐填充(Padding): JVM为了保持对象的内存地址对齐(通常是8字节对齐),可能会在对象末尾添加额外的字节。这取决于上述两部分总和后的字节是否已经满足对齐要求。

根据以上的结果,大概总结出如下规则:

  • 一个java空对象,开启压缩指针的情况下,占12字节,_mark(Markdown) 占8字节,**_class(类元指针)**占4字节,为了避免伪共享问题,jvm会按照8字节的倍数进行填充,所以会在对齐填充区填充4字节,变为16字节。

  • 在关闭压缩指针的情况下,Object默认会占16字节。其中Markdown占8字节 ,类元指针就占8字节,正好是16字节是8 的倍数,不需要填充了。

所以结论是:一般情况下,一个空的Object对象占用16字节的内存空间。

2.5 为什么重写equals()就一定要重写hashCode()方法

主要有以下几个原因:

  1. 相等性一致性

    当两个对象通过equals()方法比较时被认为是相等的,那么它们的hashCode()方法必须返回相同的值。这是为了确保当对象用作哈希表的键时,能够正确地定位和管理这些键值对。

  2. 集合操作的正确性

    在HashMap、HashSet等集合中,元素的存储位置由其hashCode()决定。如果两个逻辑上相等的对象(即equals()返回true)具有不同的哈希码,它们可能会被错误地视为两个独立的元素,导致诸如contains()、remove()等操作出现意料之外的结果。

  3. 性能考虑

    哈希表的高效查找依赖于较低的哈希冲突率。当hashCode()方法没有正确实现时,可能导致哈希冲突增多,进而影响集合的插入、查询等操作的性能。

  4. 遵守Java规范

    Java官方文档明确指出,如果你重写了equals()方法而没有重写hashCode(),那么你的类将违反Object类的通用约定,这可能导致难以预料的行为,特别是在集合框架的使用中。

以下是示例代码:

java 复制代码
package com.execute.batch.executebatch;

import java.util.HashSet;
import java.util.Objects;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    // 重写equals方法
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }

    // 重写hashCode方法
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }


    public static void main(String[] args) {
        Person person1 = new Person("Alice", 30);
        Person person2 = new Person("Alice", 30);

        HashSet<Person> set = new HashSet<>();
        set.add(person1);
        set.add(person2);

        System.out.println(person1.equals(person2)); // 应输出true,因为name和age都相同
        System.out.println(person1.hashCode() == person2.hashCode()); // 应输出true,因为hashCode也应该相同
        System.out.println(set);
    }
}

Peson类中有两个属性name和age,分别重写了equals()和hashCode()方法,此时打印结果如下:

可以看到对于相同name和age,两个对象的hashCode是一致的,HashSet中也只存在一个对象,去重了

当我们把hashCode方法注释,直接使用Object类本身的hashCode()方法时,打印结果如下:

此时会发现,两者的equals()比较为true,因为只是比较了name和age的值,而hashCode比较则返回false,因为没有重写,所以二者都是使用父类Object中继承而来的方法,各自随机生成,值并不相同,比较结果自然为false。因为HashCode不同,自然也就是两个不同的对象了,HashSet集合也就存储两个person对象,无法去重了。

三、其他特性

3.1 什么是受检异常和非受检异常

  1. 受检异常,表示在编译的时候强制检查的异常,这种异常需要显示的通过try/catch来捕捉,或者通过throws抛出,否则程序无法通过编译。

  2. 非受检异常表示编译器不需要强制检查异常,这种异常不需要显示的捕捉。

java中所有的异常都继承自Throwable类,Throwable有两个直接子类----Error和Exception

  • Error用来表示程序底层或者硬件相关的错误,这种错误和程序本身无关,比如常见的OOM异常。这种异常和程序本身无关,所以不需要检查,属于非受检异常。

  • Exception表示程序中的异常,可能是由于程序不严谨导致的,比如NullPointerException。

    • Exception下面派生了RuntimeException和其他异常,其中RuntimeException是运行时异常,属于非受检异常。
    • 其他的比如IOException和SQLException等都属于受检异常。

之所以要设置一些受检异常,比如数据库异常、文件读取异常,这些异常都是程序无法提前预料的,但是一旦出问题,就会造成资源被占用,导致程序出现问题,所以我们需要主动捕获这些异常,从而在异常情况下可以做出对应的处理,比如关闭数据库连接,释放文件流等。

3.2 failed-fast机制和failed-safe机制有什么作用

fail-fast(快速失败)和fail-safe(安全失败)机制在Java中主要用于处理迭代器在遍历集合时遇到的并发修改问题。这两种机制分别在不同的场景下提供不同的行为和性能特性。

Fail-Fast(快速失败)

  • 定义:在fail-fast机制中,如果在迭代器遍历集合的过程中,集合被其他线程修改(如添加、删除或修改元素),迭代器会立即抛出ConcurrentModificationException异常。这种机制确保了迭代器的遍历状态不会被破坏,从而避免了潜在的数据不一致或错误。

  • 作用:

    • 安全性:它提供了一种机制来检测和报告并发修改,防止迭代器继续使用可能已失效的迭代状态。
    • 强制同步:fail-fast机制鼓励程序员在多线程环境中使用适当的同步策略,如锁或其他同步工具,以确保数据的一致性和完整性。

Fail-Safe(安全失败)

  • 定义:在fail-safe机制中,迭代器在遍历集合时不会因集合被修改而抛出异常。即使在迭代过程中集合被修改,迭代器仍能安全地完成遍历,不会中断。

  • 作用:

    • 并发安全:允许在迭代过程中对集合进行并发修改,而不会中断迭代过程,这对于读多写少的场景尤其有用。

    • 性能:在某些情况下,fail-safe机制可以避免使用锁,从而提高并发性能。

实现细节

  • Fail-Fast:通常通过在集合中维护一个modCount变量来实现,每当集合被修改时,modCount就会递增。迭代器在创建时会记住modCount的初始值,在迭代过程中会检查modCount的变化,如果发现变化,就会抛出异常。
  • Fail-Safe:通常通过在迭代开始时复制整个集合,或者使用线程安全的数据结构(如CopyOnWriteArrayList)来实现,这样迭代器可以独立于集合的当前状态进行遍历。

3.3 如何理解序列化和反序列化

之所以需要序列化,是需要解决网络通信中的对象传输问题,如何把对象从一个jvm进程里跨网络传输到另外一个jvm进程里。

序列化就是把内存里面的对象转换为二进制字节流,便于存储和传输。

反序列化就是根据从文件或者网络读取的对象字节流,依据字节流里保存的对象描述信息和状态重新构建一个新的对象。

其次序列化的前提还要保证通信双方对于对象的可识别性。比如数据的格式一般转换为json或者xml,再把他们转换为数据流在网络上传输,实现跨平台和跨语言的可识别性。

序列化技术选择考虑因素:

  • 序列化后数据的大小
  • 序列化的速度
  • 是否支持跨平台、跨语言
  • 技术成熟度

3.4 什么是SPI,有什么作用?

这里的SPI是Service Provider Interface的简称,是基于接口的动态扩展机制,它的主要作用是允许在不修改现有代码的情况下,动态地加载和使用由第三方提供的服务实现。这使得应用程序或框架能够灵活地支持多种不同的服务实现,而无需在编译时硬编码具体的实现类。

SPI的工作原理大致如下:

  1. 定义接口

    开发者首先定义一个接口,这个接口描述了服务的行为和功能。

  2. 提供服务实现

    第三方开发者可以创建该接口的实现,并将其打包到自己的库中。通常,实现类的信息会通过META-INF/services目录下的一个文本文件来描述,文件名与接口的全限定名相同,文件内容是实现类的全限定名。

  3. 服务发现和加载

    在运行时,应用程序可以使用ServiceLoader类来发现并加载所有可用的服务实现。ServiceLoader会读取META-INF/services目录下的配置文件,找到所有声明的实现类,并实例化它们。

  4. 使用服务实现

    应用程序或框架可以通过ServiceLoader获取服务实现的迭代器,然后遍历并使用这些实现。

SPI机制广泛应用于各种Java框架和库中,例如JDBC驱动程序的加载、日志框架的配置等,它提供了一种标准且灵活的方式来扩展和替换组件。

这里笔者搞了个示例代码,首先我创建了一个springboot项目

在resources下手动创建META-INFO/services路径,这个路径是因为它被ServiceLoader类默认识别,从而能够自动加载和发现服务实现,我们不要去改动它,这是jdk的SPI机制默认服务发现的路径。

待扩展的接口:

java 复制代码
package com.execute.batch.executebatch.spi;

public interface GreetingService {
    void greet();
}

服务实现类ChineseGreeting:

java 复制代码
package com.execute.batch.executebatch.spi;

public class ChineseGreeting implements GreetingService {
    @Override
    public void greet() {
        System.out.println("你好!");
    }
}

服务实现类EnglishGreeting:

java 复制代码
package com.execute.batch.executebatch.spi;

public class EnglishGreeting implements GreetingService {
    @Override
    public void greet() {
        System.out.println("Hello!");
    }
}

在刚才创建的META-INFO/services下创建一个文件名com.execute.batch.executebatch.spi.GreetingService,就是待扩展接口的全限定路径名,在里面写上两个服务实现类的全限定文件名

java 复制代码
com.execute.batch.executebatch.spi.ChineseGreeting
com.execute.batch.executebatch.spi.EnglishGreeting

加载测试类ServiceLoaderDemo:

java 复制代码
package com.execute.batch.executebatch;

import com.execute.batch.executebatch.spi.GreetingService;

import java.util.ServiceLoader;

public class ServiceLoaderDemo {

    public static void main(String[] args) {
        // 加载所有的GreetingService实现
        ServiceLoader<GreetingService> loader = ServiceLoader.load(GreetingService.class);

        // 遍历并使用所有实现
        for (GreetingService service : loader) {
            service.greet();
        }
    }
}

测试结果如下:

可以看到两个服务实现类都被成功加载。

我们对springboot项目进行打包,打包成jar,可以看到classpath下自动生成了我们的META-INF/services文件路径,其中包含了创建的待扩展的全限定接口名的文件

再看下mysql的jdbc驱动

其中java.sql.Driver 就是jdk提供的jdbc驱动待扩展接口,com.mysql.cj.jdbc.Driver就是mysql实现了Drive接口的服务实现扩展类。

再比如logback日志框架,就扩展了slf4j的接口SLF4JServiceProvider

实际开发中当我们开发一个基础组件时,为了方便第三方扩展,就可提供一个接口规范,供第三方使用SPI机制去扩展自己实现,基本原理如上,应该很好理解。

3.5 finally语句快一定会执行吗?

这个问题挺有意思的,再没有学习研究之前,笔者也是任务finall语句块实在try catch后一定执行的,但是研究一番后,我有了不一样的认识,实际是否执行还得分情况。具体如下:

  • 如果try块中的代码正常执行结束,没有抛出任何异常,finally块会紧接着try块之后执行。
  • 如果try块中的代码抛出了异常,即使这个异常被catch块捕获和处理,finally块也会在catch块执行完后执行。
    即使try块中的代码抛出的异常没有被catch块捕获,导致程序异常终止,finally块仍然会被执行,除非整个JVM非正常关闭。

但是,有以下几种情况finally块可能不会被执行:

  • 如果在try或catch块中调用了System.exit(int status)方法,这会立即终止JVM,导致finally块不被执行。
  • 如果JVM因为外部原因(如操作系统杀掉进程)而非正常关闭,finally块也可能不被执行。

总的来说,只要程序控制流正常进行,finally块几乎总是会被执行,这是它用来释放资源、清理环境的核心价值所在。

下面是一些示例代码:

  1. 正常执行无异常
java 复制代码
package com.execute.batch.executebatch.finallyTest;

public class FinallyExample {
    public static void main(String[] args) {
        try {
            System.out.println("Inside try block");
        } finally {
            System.out.println("Finally block executed");
        }
        System.out.println("After try-finally block");
    }
}

结果如下,finally正常执行

  1. 异常被捕获
java 复制代码
package com.execute.batch.executebatch.finallyTest;

public class FinallyExample {
    public static void main(String[] args) {
        try {
            System.out.println("Inside try block");
            throw new RuntimeException("Exception thrown");
        } catch (RuntimeException e) {
            System.out.println("Caught exception: " + e.getMessage());
        } finally {
            System.out.println("Finally block executed");
        }
    }
}

结果如下,finally正常执行了

  1. 异常未被捕获
java 复制代码
package com.execute.batch.executebatch.finallyTest;

public class FinallyExample {
    public static void main(String[] args) {
        try {
            System.out.println("Inside try block");
            throw new Error("Uncaught error");
        } finally {
            System.out.println("Finally block executed");
        }
    }
}

结果中可以看到finally任然正常执行

  1. 使用 System.exit()
java 复制代码
package com.execute.batch.executebatch.finallyTest;

public class FinallyExample {
    public static void main(String[] args) {
        try {
            System.out.println("Inside try block");
            System.exit(0);
        } finally {
            System.out.println("Finally block executed");
        }
    }
}

这种就属于强制退出了,finally就不会执行

3.6 内存溢出和内存泄漏区别?

内存溢出(Out of Memory)

内存溢出指的是程序运行时请求的内存超过了系统所能提供的最大内存限制。这种情况通常发生在:

  • 堆内存溢出:当Java堆空间不足,无法再分配新的对象时,会抛出 OutOfMemoryError。这可能是由于创建了过大的数组或对象,或者GC(Garbage Collector)未能回收足够的内存。

  • 栈内存溢出:当线程的栈空间耗尽时,比如递归调用过深,也会引发内存溢出。

下面这段代码,不停的创建对象,最后会把jvm内存耗尽,报出OOM异常

java 复制代码
public class OutOfMemoryExample {
    public static void main(String[] args) {
        while (true) {
            byte[] b = new byte[1024 * 1024]; // Allocate 1 MB each time
        }
    }
}

内存泄漏(Memory Leak)

内存泄漏指的是程序在动态分配内存后未能释放已不再使用的内存,导致这部分内存无法被GC回收。随着时间的推移,这种泄漏积累的未释放内存会逐渐消耗系统的可用内存,最终可能导致性能下降或程序崩溃。

下面的代码就是内存一直占用,资源不释放

java 复制代码
public class MemoryLeakExample {
    private List<byte[]> leaks = new ArrayList<>();

    public void leak() {
        leaks.add(new byte[1024 * 1024]); // Allocate 1 MB and never release
    }

    public static void main(String[] args) {
        MemoryLeakExample example = new MemoryLeakExample();
        while (true) {
            example.leak();
        }
    }
}
相关推荐
一只特立独行的猪6112 分钟前
Java面试——集合篇
java·开发语言·面试
讓丄帝愛伱1 小时前
spring boot启动报错:so that it conforms to the canonical names requirements
java·spring boot·后端
weixin_586062021 小时前
Spring Boot 入门指南
java·spring boot·后端
Dola_Pan4 小时前
Linux文件IO(二)-文件操作使用详解
java·linux·服务器
wang_book4 小时前
Gitlab学习(007 gitlab项目操作)
java·运维·git·学习·spring·gitlab
蜗牛^^O^5 小时前
Docker和K8S
java·docker·kubernetes
从心归零6 小时前
sshj使用代理连接服务器
java·服务器·sshj
王中阳Go6 小时前
字节跳动的微服务独家面经
微服务·面试·golang
IT毕设梦工厂7 小时前
计算机毕业设计选题推荐-在线拍卖系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
Ylucius7 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习