公告

👇微信扫码添加好友👇

Skip to content

10-31

什么是原子操作?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。 在Java中可以通过锁和循环CAS的方式来实现原子操作。CAS操作—— Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作。

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。 int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。

为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。 到JDK1.5,java.util.concurrent.atomic 包提供了 int 和 long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

10-30

为什么使用Executor框架?

  1. 能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
  2. 可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
  3. 框架中已经有定时、定期、单线程、并发数控制等功能。

综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。

10-29

什么是 Executor 框架?

线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。线程用于执行异步任务, 单个的线程既是工作单元也是执行机制,从JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架诞生了,他是一个用于统一创建与运行的接口。 Executor框架实现的就是线程池的功能。

Executor框架包括3大部分:

  • 任务。也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口;
  • 任务的执行。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。
  • 异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。

10-28

什么是线程组?

线程组,顾名思义,就是线程的组,逻辑类似项目组,用于管理项目成员,线程组就是用来管理线程的。线程组中可以有线程对象, 也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。 每个线程都会有一个线程组,如果没有设置将会有些默认的初始化设置。

虽然线程组看上去很有用处,实际上现在的程序开发中已经不推荐使用它了,主要有两个原因:

  1. 线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题), 已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。

  2. 线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。

虽然线程组现在已经不被推荐使用了(推荐使用线程池),但是它在线程的异常处理方面还是做出了一定的贡献。当线程运行过程中出现异常情况时, 在某些情况下JVM会把线程的控制权交到线程关联的线程组对象上来进行处理。所以对线程组的了解还是有一定必要的。

10-27

产生死锁的必要条件。

  • 互斥条件:共享资源被一个线程占用
  • 请求与保持条件(占有且等待):一个进程因请求资源而阻塞时,对已获得的资源保持不释放
  • 不剥夺条件:进程已获得资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:多个线程之前循环等待资源,必须是循环的互相等待

10-26

什么是上下文切换?

由于时分复用的存在,CPU 为每个线程分配时间片并轮转,从一个线程切换到另一个线程,这个过程需要先保存当前线程的状态, 然后加载新线程的状态,这就是上下文切换。

上下文切换通常是计算密集型的,会消耗大量 CPU 的时间。而 Linux 的一个特性就是上下文切换和模式切换的消耗非常少。

10-25

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

要停止一个正在运行的线程,可以使用 Thread.interrupt() 方法来中断线程,或者使用 Thread.stop() 方法来停止线程。

10-24

线程不安全的本质什么?

由于各个硬件设备的数据处理速度有极大差异,为了处理 CPU、内存、I/O 设备之间的速度差异,计算机体系结构、操作系统以及程序编译过程都做出了相应的优化。

  • CPU 增加了缓存,以均衡与内存之间的速度差异,这导致了 可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,这导致了 原子性问题
  • 编译器优化指令执行次序,使得缓存能够得到更加合理的利用,这导致了 有序性问题

10-23

AOP 的实现方式?

  • 静态代理: 代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。
  • 动态代理: 代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。

10-22

什么是 AOP?

AOP (Aspect oriented programming)区别于OOP (面向对象编程),是对 OOP 的一种补充。 它会将事务管理、日志记录等公共业务逻辑封装为切面单独处理,与业务代码分割。减少重复性代码,降低系统耦合性。

10-21

什么是 IOC?

IOC (Inversion of Control)控制反转。指对象的生命周期控制通过反射由 Spring 容器 处理,包括对象的初始化、创建、销毁等。 能够降低类之间的耦合。

IOC 又称为 DI(依赖注入),是指在 Spring 创建对象的过程中,把对象依赖的属性注入到对象中。有两种方式:构造器注入和属性注入。

10-20

请简单介绍JAVA8新特性?

JAVA8 已经名义上停止维护了,但是 JAVA8 新特性还是面试经常会问到的点。就是为了考察 函数式编程 以及 stream 流 的掌握情况。

  • Lambda 表达式: JAVA8 之后允许使用更简洁的箭头函数创建函数式接口的实例,在功能上与匿名内部类类似,但是更加简洁。

  • Stream 流操作: Stream 流允许开发者对集合或数组进行链状流式操作,基于函数式编程模型。分为中间操作及终结操作。

  • Optional 类: 使用 Optional 可以写出更优雅的代码来避免空指针异常。 Optional 类似于包装类,将具体的数据封装到Optional对象内部。我们可以使用 Optional 中封装好的方法操作封装的数据。优雅的避免空指针异常。

10-19

Integer 和 int 的区别?

Java 虽然宣称一切都是对象,但原始数据类型是例外。 int 是整形数字,是 java 的8个原始数据类型(Primitive Types)(char、boolean、byte、short、int、long、float、double)之一。 在 java API 中有对应的包装类,分别是Character、Boolean、Byte、Short、Integer、Long、Float、Double。

Integer 是 int 对应的包装类,它有一个int类型的字段存储数据,并且提供了基本操作,比如数学运算、int和字符串之间转换等。 在 java 5 中引入了自动装箱和自动拆箱功能(boxing/unboxing),java 可以根据上下文,自动进行转换,极大地简化了相关编程。 javac自动把装箱转换为Integer.valueOf(),把拆箱替换为Integer.intValue()。

自动装箱实际上算是一种语法糖。什么是语法糖?可以简单理解为java平台为我们自动进行了一些转换,保证不同的写法在运行时等价, 他们发生在编译阶段,也就是生产的字节码是一致的。

原始数据类型和 java 泛型并不能配合使用。因为 java 的泛型某种程度上可以算作伪泛型,它完全是一种编译期的技巧, java 编译期会自动将类型转换为对应的特定类型。这就决定了使用泛型,必须保证相应类型可以转换为 Object。

10-18

抽象类和接口的区别?

  1. 一个类可以实现多个接口,但只能继承一个抽象类。接口本身可以通过 extends 关键字扩展多个接口。

  2. 抽象类可以有构造方法,接口中不能有构造方法。

  3. 抽象类中不一定都是抽象方法,也可以全是具体方法(非抽象方法)。在 Java8 之前,接口中的所有方法必须是抽象的。但Java8的时候,接口可以有默认方法和静态方法。

  4. 接口中只能有 static、final 变量,不能有其他变量,而抽象类中不一定。

  5. 接口中的方法和变量默认修饰符为 public(写不写都是public), 而抽象类中的方法可以是 public,protected 和默认类型(抽象方法就是为了被重写所以不能是 private 修饰)

10-17

构造器是否可被重写?

重写是子类覆盖父类的方法,而构造器名称与类名相同,不可能存在重写的概念。

但是在一个类中,是可以存在多个参数不同的构造器的,这是构造器的重载。

10-16

请描述 & 和 && 的区别。

相同点:&&& 都可以用作逻辑与的运算符,表示逻辑与(and)。

不同点:

1、&& 具有短路的功能,而 & 不具备短路功能。

2、当 & 运算符两边的表达式的结果都为 true 时,整个运算结果才为 true。而&&运算符第一个表达式为 false 时,则结果为 false,不再计算第二个表达式。

3、& 还可以用作位运算符,当 & 操作符两边的表达式不是boolean类型时,& 表示按位与操作, 我们通常使用 0x0f 来与一个整数进行&运算,来获取该整数的最低4个 bit 位,例如:0x31 & 0x0f 的结果为 0x01。

10-15

请描述面向对象的特征。

面向对象有三大特征:继承封装多态

如果是四大特征:继承封装多态抽象

继承: 在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容, 并可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。比如,遗产的继承。 封装: 面向对象的封装性指的是将对象封装成一个高度自治和相对封闭的个体,对象状态(属性)由这个对象自己的行为(方法)来读取和改变 多态: 多态指的是程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定, 即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底在哪个类中实现的方法,必须在由程序运营期间才能决定。 抽象: 抽象就是找出一些事物的相似和共性之处,然后将这些事物归为一个类,这个类只考虑这些事物的相似和共性之处, 并且会忽略与当前主题和目标无关的那些方面,将注意力集中在与当前目标有关的方面。就是把现实生活的对象,抽象为类。

10-14

Java 中 throw 和 throws 区别?

  • throw 是语句抛出的一个异常。
  • throws 是方法可能抛出异常的声明。

两者都是“消极处理异常”的方式(指抛出或可能抛出异常)。

10-13

运行时异常与受检异常有何异同?

所有异常都继承自 java.lang.Throwable,它有两个直接的子类 Error 与 Exception。

Java Compiler 要求所有的 Exception 要么被 catch ,要么被 throw,除非这是一个 RuntimeExeption。

**受检异常(Checked Exception)**是在编译时期有编译器检测的异常,该异常必须要被处理。受检异常继承于Exception。

**非受检异常(Unchecked Exception)**是在运行时期的异常,即编译器不会检测异常,需要时也可以捕获异常。非受检异常继承于RuntimeException。

10-12

是否可以继承String类?

String 类是不能被继承的,因为他是被final关键字修饰的。

java
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
  ......
}

10-11

String 类的长度有限制吗?

String 是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数且 String 类中返回字符串长度的方法 length() 的返回值也是 int , 所以通过查看 java 源码中的类 Integer 我们可以看到 Integer 的最大范围是2^31 -1, 由于数组是从0开始的,所以数组的最大长度可以使【0~2^31】通过计算是大概4GB。

但是通过翻阅 java 虚拟机手册对 class 文件格式的定义以及常量池中对 String 类型的结构体定义我们可以知道对于索引定义了 u2,就是无符号占2个字节, 2个字节可以表示的最大范围是 2^16 -1 = 65535。 但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。 超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。

10-10

什么是节流和防抖?应用场景有哪些?

在前端开发过程中,当函数绑定到某些可能会持续触发的事件中时, 可能会造成性能和资源的浪费。如鼠标的移动、提交按钮可能被用户快速重复的点击等。 节流防抖就是为了解决这一问题。

  • 防抖(debounce): 防抖指事件触发时,它将在指定时间后执行,如果这段时间内再次被调用,则重新计算执行时间。 通常用于滚动事件、调整窗口大小、鼠标移动、窗口搜索建议等。
javascript
function debounce(func, delay) {
    let timeoutId;
    return function (...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            func(...args);
        }, delay);
    };
}

// 使用防抖函数
const debouncedFunction = debounce(() => {
    console.log('Debounced function called.');
}, 1000);

// 模拟事件触发
setInterval(() => {
    debouncedFunction();
}, 200);  // 防抖后,该函数触发后会等待1秒,如果1秒内再次触发,则重新计时
  • 节流(throttle): 节流指连续触发多次的事件在指定时间范围内只执行一次,进而限制函数的执行频率。 通常用于自动保存、延时执行等。
javascript
function throttle(func, delay) {
  let lastCall = 0;
  return function (...args) {
    const now = new Date().getTime();
    if (now - lastCall < delay) return;
    lastCall = now;
    func(...args);
  };
}

// 使用节流函数
const throttledFunction = throttle(() => {
  console.log('Throttled function called.');
}, 1000);

// 模拟事件触发
setInterval(throttledFunction, 200);  // 节流后,该函数每秒最多执行一次

总的来说,节流和防抖都是为了限制函数执行的频率,以优化性能、减少不必要的计算或请求,提高用户体验。选择使用哪种取决于具体的需求和事件特性。

10-9

请描述 static 修饰符。

static 关键字可以用于类、方法、属性等。

静态字段

如果将一个字段定义为static,那么这个字段并不会出现在每个类的对象中。每个静态字段只有一个副本。可以认为静态字段属于类,而不属于单个对象。

静态常量

JAVA中没有用于修饰常量的关键字,所以常量的修饰通常使用 public static final 三个关键字共同修饰。使其静态加载且不允许修改。

静态方法

静态方法是不操作对象的方法。例如 Math.pow(x,a)会计算 x 的 a 次幂。它不会使用任何 Math 对象来完成该方法调用。它没有隐式参数。

工厂方法

类似于 LocalDateNumberFormat 的类使用静态工厂方法来构造对象,就是静态方法:

Java
LocalDate localdate = LocalDate.now(); 
LocalDate localdate = LocalDate.of(2020, 5, 13);  
System.out.println(NumberFormat.format(0.1));

main 方法

调用静态方法不需要任何对象,与 Math.pow 同理,main 方法也是一个静态方法,启动程序时没有任何对象,将执行 main 方法并构造程序所需要的对象。

10-8

请描述修饰符、访问修饰符,非访问修饰符。

  • 修饰符(Modifiers):指用于修饰类、方法。变量等的关键字,用于置顶元素的访问类型。作用域、权限、继承性、抽象性等。修饰符分为访问修饰符和非访问修饰符。

  • 访问修饰符(Access Modifiers): 访问修饰符控制类、方法、变量等的访问权限,共有以下四中:

    NameDescription
    public能被任何类访问。
    protected同一包课件,其他包的子类可见。
    default默认,只能在同一包中被访问。
    private只能在同一类中访问。
  • 非访问修饰符(Non-Access Modifiers): 非访问修饰符用于实现其他功能,包括以下几种类型:

NameDescription
final表示不可继承的类、不可重写的方法或不可修改的变量。
abstract表示抽象类或抽象方法,不能直接实例化抽象类。
static静态资源关键字,用于创建类级别的变量或方法,而非实例级别。
synchronize用于同步代码块,保证多线程环境下的同步访问。
transient用于表示不需要序列化的变量。
volatile用于表示多线程共享变量,保证可见性但不保证原子性。
strictfp用于强制浮点运算遵循IEEE 764 标准,确保跨平台的浮点计算结果一致。
sealed密封类。用于控制哪些类可以对该类进行拓展。
record用于创建不可变的数据类。

10-7

什么是方法签名?

在上一篇中说到,重写必须要求方法签名相同,这里的方法签名(Method Signature),指的是方法名称及参数类型列表。如:

java
int sum(int a, int b){return a + b;}

该方法的方法签名为: sum(int, int)

10-6

重载和重写是什么意思?有什么区别?

重载(Overloading)指在同一个类中可以定义多个参数列表不同的同名方法,其互相的关系是重载。它是编译时多态(静态绑定),根据参数列表选择调用对应的方法。

重写(Overriding)指子类可以重新定义父类中非私有的方法,方法签名必须相同。子类可以通过重写父类的方法来实现自己的特定行为。 它是运行时多态(动态绑定),在运行时根据对象的实际类型选择调用对应的方法。

10-5

String、StringBuilder、StringBuffer 的区别及使用场景

  1. 可变性
  • String 不可变, StringBufferStringBuilder 可变
  1. 线程安全
  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

10-4

请描述 instanceof 关键字

instanceof 是 JAVA 的一个二元操作符,类似于大于小于号,用于测试其左边的对象是否是它右边对象的实例, 返回 Boolean 类型。在下方的 10-3 的示例中,String 类的 equals 方法中使用了 instanceof 关键字对比较的对象进行实例检查,并转换为 String 类型。

10-3

==equals 的区别

两者的作用都是判断是否相等。但是 == 是运算符,equals 是继承于 Object 类的一个方法。

对于 == 来讲,如果比较基本数据类型,则比较其存储的值。如果比较引用数据类型,则比较其对象的地址值是否相等。

对于 equals 方法来讲,其本身并不提供基本数据类型的比较, 但是可以使用 Objects 类提供的静态 equals 进行比较,如果是基本数据类型,其实现也是使用 ==

java
public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

继承于 Object 类的 equals 方法也是使用 == 进行比较:

java
public boolean equals(Object obj) {
    return (this == obj);
}

所以要比较两个引用数据类型,必须重写其 equals 方法才能实现对于特定条件的比较,这里以常用的 String 类进行举例,其重写的 equals 方法如下:

java
public boolean equals(Object anObject) {
    // 判断其地址值相同,直接返回 TRUE
    if (this == anObject) {
        return true; 
    }
    // 类型检查,判断是否为 String 的实例,如果是,anObject 会被转化为 aString 表示的 String 类型
    return (anObject instanceof String aString)
        	// 是否压缩(紧凑表示方式),如果压缩则检查其编码是否相同
        	// 这里 aString 是规范的命名方式。。。
            && (!COMPACT_STRINGS || this.coder == aString.coder)
        	// 遍历比较字符数组
            && StringLatin1.equals(value, aString.value);
}

10-2

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

答案:FALSE

这道题跟 01+0.2 != 0.3 是一样的,由于在计算机系统中使用二进制浮点数无法精确的表示诸如 10/3 等无限循环的十进制分数的值, Java中的doublefloat类型使用IEEE 754标准来表示浮点数,这种表示方式会导致一些十进制分数无法精确地表示。 因此,当你尝试在Java中表示0.3时,可能会得到一个非常接近 0.3 的值,但不会是精确的 0.3。

要想精确的表示十进制小数,可以使用 BigDecimal 类进行精确的十进制数学运算,但是会更为复杂并造成额外的开销。

10-1

请详述 JDK JRE JVM 等的关系

声明

该内容由 CHATGPT 生成

  1. JDK(Java Development Kit)

    JDK 是Java开发工具包,它是Java开发人员用来编写、编译、调试和运行Java程序的核心工具。

  2. JDK包括了Java编译器(javac)、Java解释器(java)、Java文档生成器(javadoc)等工具,以及Java开发所需的库和资源文件。

  3. JRE(Java Runtime Environment)

    JRE是Java运行时环境,是Java程序的运行环境,用于执行已经编译好的Java字节码。 JRE包括了Java虚拟机(JVM)、Java类库(Java API)和运行时所需的其他组件。JRE提供了在特定平台上运行Java程序所需的基本功能。

  4. JVM(Java Virtual Machine)

    JVM 是Java虚拟机,是Java程序的运行引擎,负责将Java字节码解释或编译成特定平台的本地机器代码,并执行程序。 JVMJRE 的一部分,它是一个虚拟的计算机,可以在不同的硬件和操作系统上运行Java程序。

关系总结

  • JDK包括JRE,JRE包括JVM。
  • JDK提供了开发、编译、调试等工具,以及JRE所需的库和资源。
  • JRE提供了JVM和Java API,用于运行Java程序。
  • JVM是Java程序的运行引擎,它执行Java字节码并负责内存管理、垃圾回收等。
  • Java程序首先由JDK编译生成Java字节码,然后JRE中的JVM解释或编译Java字节码并执行。