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

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

         
    
    • 1

    (3)函数式接口:

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

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

    在这里插入代码片
    
    • 1

    (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));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    (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锁是如何实现的?

  • 相关阅读:
    【Unity3D】激光雷达特效
    项目成本管理的重要性:为了削减成本,马斯克裁员50%
    labelme安装
    顶级“Redis 笔记”, 缓存雪崩 + 击穿 + 穿透 + 集群 + 分布式锁,NB 了
    多输入多输出 | MATLAB实现PSO-LSSVM粒子群优化最小二乘支持向量机多输入多输出
    渗透测试——formatworld(2)
    算术优化与阿奎拉鹰优化的混合算法
    从油猴脚本管理器的角度审视Chrome扩展
    Java回顾-String/StringBuilder/StringBuffer
    windows下使用pytorch进行单机多卡分布式训练
  • 原文地址:https://blog.csdn.net/qq_42785250/article/details/133694198