【面试高高手】—— Java进阶

1. 什么是异常?

程序中的错误统称为异常。

2.你是如何理解Java中的异常体系的 ?

  • Throwable是所有异常类的父类;
    • Error 程序不可处理,如内存溢出,JVM异常
    • Exception 程序可处理。
    • 可查异常:最典型的是IO类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
    • 不可检查异常:
      • 运行时异常 使用try…catch捕获
      • 非运行时异常 编译不通过。

3.Error和Exception的区别是什么 ?

  1. Error类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA应用程序也不应对这类错误进行捕获,一旦这类错误发生,应用程序通常会被终止,仅靠应用程序本身无法恢复;
  2. Exception类型的异常是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。

4.throw 和 throws的区别是什么 ?

  • throw关键字用来抛出方法或者代码块中的异常对象,检查异常和非检查异常都可以被抛出,在方法内部使用;
  • throws关键字用来标识该方法可以抛出的异常类型列表,在方法定义时,在参数列表之后和方法体之前进行定义。

5.Java中的常见异常有哪些 ?

  1. NullPointerException 空指针异常:调用未实例化的null引用,引发该异常;
  2. ClassNotFoundException 找不到类异常:按照类的完全限定名加载一个不存在的类(如反射时),会引发该异常;
  3. NumberFormatException 数字格式化异常:将字符串转换为数字时,如果该字符串中包含非数字内容时,会引发该异常;
  4. IndexOutOfBoundsException 下标越界异常:数组或字符串下标超出范围时,会引发该异常;
  5. IllegalArgumentException 不合法参数异常:传入参数不合法,引发该异常;
  6. ClassCastException 类型转换异常:转换不符合的Class类型,引发该异常;
  7. SQLException SQL异常:在操作数据库时,产生SQL语法错误时,会引发该异常;
  8. IOException 读写异常:对文件流进行IO读写操作发生错误时,会引发该异常;
  9. NoSuchMethodException 方法不存在异常:找不到调用方法,引发该异常。

6. 说说你对内存可见性的理解?

可以举例说明,一个公共变量a,三个线程,一个线程修改了a的值,其他两个线程可能看不到a变化后的值,这就是内存可见性问题。
原因是:为了提高处理速度,每个线程都会在 CPU 中维护一份共享变量的本地缓存,而线程对共享变量的所有操作都会在自己的本地缓存中进行。如果线程 A 更改了一个共享变量,线程 B 有可能看不到线程 A 的修改
解决方法:

  • 使用volatile关键字:
    将变量声明为volatile,这会告诉Java虚拟机确保所有线程都能看到最新的值。
    当一个线程修改了volatile变量的值,这个变化会立即被写入主内存,并且其他线程在读取该变量时会从主内存中获取最新值。

  • 使用synchronized关键字:
    使用synchronized块来对读写操作进行同步,确保同一时间只有一个线程能够访问共享变量。
    当一个线程获取了锁并修改了共享变量后,其他线程必须等待该线程释放锁才能访问该变量,这样可以确保可见性。

  • 使用java.util.concurrent包中的工具类:
    Java提供了一些并发工具类,如AtomicInteger、CountDownLatch、CyclicBarrier等,它们可以用来处理多线程可见性问题,而无需手动编写同步代码。
    这些工具类提供了原子操作和同步机制,可以确保对共享变量的修改对其他线程可见。

7.说下你对volatile关键字的理解?

volatile 是 Java 中的关键字,用于修饰变量。它的主要作用是确保多线程环境下的可见性和有序性,这意味着当一个线程修改了 volatile 变量的值时,其他线程可以立即看到这个修改。
volatile关键字的作用:

  • 可见性(Visibility): 在多线程环境下,当一个线程修改了 volatile 变量的值,这个变化对其他线程是可见的。这意味着当一个线程修改了 volatile 变量后,其他线程不会读取到过期的缓存值,而是能够看到最新的值。
  • 禁止指令重排序(Ordering): volatile 关键字还可以防止编译器和处理器对指令进行重排序。这确保了 volatile 变量的读写操作按照代码的顺序执行,而不会出现意外的指令重排。
  • 不保证原子性(Atomicity): volatile 关键字仅确保可见性和有序性,但不保证原子性。如果多个线程同时对同一个 volatile 变量进行写操作,可能会出现竞态条件。对于需要原子性操作的场景,应该使用 synchronized 或 java.util.concurrent 中的原子类。

适用场景: volatile 适用于一些简单的标志位或状态标识的操作,例如线程之间的信号通知。它不适合复杂的操作,如累加操作。

8.说下Java8有哪些新特性?

(1)接口的默认方法和静态方法:之前接口只能够做方法的声明,没有实现,Java8以后允许接口有一个默认的实现,必须使用default修饰符标记;

default void test(){
    
    }
static void test2(){
    
    }
  • 作用:
    • 向已有接口添加新方法:默认方法允许在已有的接口中添加新方法,而不会破坏已经实现了该接口的类。在Java 8之前,如果要向接口中添加新方法,所有实现该接口的类都必须提供该方法的实现,这可能导致破坏现有代码。
    • 接口的扩展性:默认方法提高了接口的扩展性。新的方法可以添加到接口中,而不会打破已有的实现类。这对于面向接口的编程非常有用,因为它允许接口逐渐演进而不影响已有代码。
    • Lambda 表达式和函数式编程:默认方法的引入与Lambda表达式一起,使接口更容易用于函数式编程。例如,Java标准库中的java.util.function包中的函数式接口使用了默认方法,这使得在使用Lambda表达式时,可以只实现一个或少数几个抽象方法。

(2)Lambda表达式:Lambda最直观的是将代码变得整洁。

     

(3)函数式接口:

  • Comparetor
  • Consumer
  • predicate(断言式接口)
  • Supplier
  • Function(功能型接口)

(4)方法引用:是用来直接访问类或者实例中的方法或者构造方法,这样代码的可读性会更高一些。
就是使用::来调用类中的方法:

在这里插入代码片

(5)Stream流:它允许你以声明式的方式处理数据集合。

  • 特点:
    • 内部迭代:
    • 只能遍历一次:当流遍历完成后,这个流就被消费掉了。
    • 可以并行处理: select.stream().parallel()

(6)Optional:为了解决空指针异常。并且让代码更加简洁,使用它我们不需要显式的进行空指针检测。
Optional + lambda实现比较字符串,并找到最长的字符串

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class OptionalLambdaExample {
    
    

    public static void main(String[] args) {
    
    
        List<String> stringList = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");

        Optional<String> longestString = stringList.stream()
                .reduce((s1, s2) -> s1.length() > s2.length() ? s1 : s2);

        longestString.ifPresent(s -> System.out.println("最长的字符串是: " + s));
    }
}

(7)Date/Time
(8)重复注解
(9)扩展注解的支持
(10)Base64
(11)JavaFx

9.请你说一下Java中Sync和lock的区别。

  • 实现方式:

synchronized 是Java的关键字,直接内置在Java语言中。您可以使用synchronized关键字来实现同步块或同步方法。
Lock 是一个接口,它在java.util.concurrent.locks包中定义。Java提供了多种Lock的实现,包括ReentrantLock、ReadWriteLock等。您可以使用这些Lock实现来管理同步。

  • 灵活性:

Lock 提供了更多的灵活性。您可以使用Lock接口的不同实现来满足特定的同步需求。例如,ReentrantLock支持可重入锁,而ReadWriteLock支持读写锁。
synchronized更简单,但在某些情况下可能不够灵活,例如无法轻松实现尝试锁定、定时锁定等功能。

  • 锁定粒度:

synchronized关键字用于锁定整个方法或代码块,这可能会导致性能问题,特别是在高并发情况下。
Lock允许您更细粒度地控制锁的范围,可以只锁定需要同步的关键部分,从而提高并发性能。

  • 异常处理:

使用synchronized关键字时,如果发生异常,锁将自动释放。
使用Lock时,您需要在try-finally块中手动释放锁,以确保在发生异常时锁定资源得到释放。

  • 条件等待:

Lock接口提供了Condition对象,可以用于实现条件等待和通知机制。这使得线程能够更灵活地等待某些条件的发生,而不需要一直忙等。
总之,synchronized适合简单的同步需求,而Lock适用于更复杂、灵活的同步需求。选择哪种同步方式取决于您的具体需求和性能考虑。在Java并发编程中,通常建议优先使用Lock接口,因为它提供了更多的控制和灵活性。但要注意,Lock使用起来相对复杂,需要小心处理异常和锁定资源的释放。

10.请说一下Java的线程中sleep和wait的区别?

sleep() 用于线程的暂时休眠,不释放锁,通常用于时间延迟。
wait() 用于线程之间的等待和唤醒机制,会释放锁,必须在synchronized块中使用。
sleep()是Thread类的方法,而wait()是Object类的方法。

11. 请你说一下JVM的指令重排?

12. JVM的分区有哪些?类对象的晋升方式是怎么样的?

(1)新生代: 新生代是堆内存的一部分,主要用于存储新创建的对象。新生代:
- Eden空间:新创建的对象首先分配到Eden空间。
- Survivor空间(S0和S1):当Eden空间填满时,存活下来的对象会被移动到Survivor空间。Survivor空间有两个,通常标记为S0S1,它们交替用作对象的复制和垃圾回收。经过多次复制和存活的对象最终会被晋升到老年代。
(2)老年代: 老年代用于存储长时间存活的对象。当对象在新生代经过多次复制后仍然存活,它们会被晋升到老年代。老年代中的对象在垃圾回收时会经历更长的生命周期。
(3)永久代: 永久代用于存储类的元数据信息,如类定义、方法信息、常量池等。它的大小在JVM启动时被固定分配,不会自动扩展。在旧版本的JVM中,永久代可能会导致内存溢出问题,因此在JVM 8中被元数据区所取代。
(4)元空间: 元数据区是用于存储类的元数据信息的内存区域。与永久代不同,元数据区的大小是动态分配的,它可以根据应用程序的需求而自动扩展或收缩。在JVM 8及更高版本中,元数据区取代了永久代,这解决了永久代导致的一些内存溢出问题。

类对象的晋升方式:

(1)对象分配到新生代(Eden空间): 当一个对象被创建时,它通常被分配到新生代的Eden空间。这是新对象的初始分配区域。

(2)Minor Garbage Collection(新生代垃圾回收): 新生代会定期进行垃圾回收,通常采用复制(Copying)算法。在这个过程中,存活的对象会被复制到Survivor空间(S0或S1),而不存活的对象会被回收。

(3)对象在Survivor空间中晋升: 如果对象在新生代经历了一定次数的垃圾回收后仍然存活,它会被晋升到老年代。这个阈值通常由JVM参数控制。

(4)Major Garbage Collection(老年代垃圾回收): 老年代的垃圾回收发生相对较少,因为其中存储的对象通常具有较长的生命周期。老年代的垃圾回收通常使用标记-清除(Mark and Sweep)算法。

(5)Full Garbage Collection(全局垃圾回收): 如果老年代垃圾回收无法回收足够的内存,JVM可能会触发全局垃圾回收,它会回收整个堆内存,包括新生代和老年代。

晋升到老年代的对象经历了多次新生代垃圾回收,这意味着它具有较长的生命周期,通常是应用程序中的长期存活对象。这种分代垃圾回收策略有助于提高垃圾回收的效率,因为大多数对象都在新生代很快被回收,而只有一小部分对象会晋升到老年代,减少了老年代的垃圾回收频率,从而减少了应用程序的停顿时间。

13. Java中常见的锁有哪些?

14. 说一下synchronized锁是如何实现的?

猜你喜欢

转载自blog.csdn.net/qq_42785250/article/details/133694198