上周,一位朋友去小米的面经,很多都是开放性问题,真的八股文好像也不顶用了。
1、Java 编程语言它有什么样的特点吗?Java 是一种广泛应用的编程语言,具有许多特点和优势,使其在各种领域都得到了广泛的应用。以下是 Java 编程语言的一些主要特点: 跨平台性(Write Once, Run Anywhere):
Java 使用虚拟机(JVM)作为中间层,使得 Java 程序在不同平台上具有良好的可移植性和跨平台性。 面向对象:
Java 是一种支持面向对象编程范式的语言,提供类、对象、继承、封装、多态等面向对象特性。 简单易学:
Java 设计简洁、清晰,语法与C/C++相似,使得初学者可以相对快速地学习和掌握。 强类型:
Java 是一种强类型语言,要求变量严格定义类型,在编译时进行类型检查,有助于提高程序的稳定性和安全性。 自动内存管理(垃圾回收):
Java 提供自动内存管理机制,通过垃圾回收器(Garbage Collector)自动管理内存,减少内存泄漏问题。 丰富的库支持:
Java 提供了丰富的类库(Java API),包括标准类库和第三方类库,支持各种应用开发需求。 多线程支持:
Java 内置对多线程编程的支持,通过实现 Runnable 接口或继承 Thread 类,可以轻松实现多线程编程。 安全性:
Java 提供安全管理机制,如类加载器、安全管理器等,可以保护系统免受恶意代码攻击。 动态性:
Java 支持动态加载和执行新的代码,如反射机制和 Java 动态代理。 广泛应用:
Java 在企业应用开发、移动应用开发(Android 开发)、嵌入式系统、大数据处理、云计算等领域得到广泛应用。 开源社区和生态系统:
Java 有庞大的开源社区,提供丰富的开源库和框架,如 Spring、Hibernate、Apache Commons 等,为开发者提供了强大的支持。
Java 的这些特点使其成为一种受欢迎的编程语言,适用于各种类型的应用程序开发,尤其在企业级应用和大型系统开发中得到广泛应用。
2、那你觉得它为什么可以实现一次编写到处运行。一次编写到处运行是 Java 语言的一个核心特点,主要得益于 Java 虚拟机(JVM)的存在。
下面是我向您解释为什么 Java 可以实现这一特性: 字节码和 JVM:Java 源代码首先被编译成字节码,而不是特定硬件架构的机器代码。这种字节码可以在任何支持相同 Java 版本的 JVM 上运行。 跨平台性:JVM 充当了 Java 程序与具体操作系统之间的中间层,负责将字节码转换为不同操作系统所需的机器码,从而实现跨平台性。 抽象层的实现:JVM 提供了一个抽象层,隐藏了与特定硬件平台和操作系统相关的具体细节,使得 Java 程序能够在不同环境中保持一致的行为。 JVM 实现:JVM 在不同操作系统上有对应的实现,例如 Oracle 的 HotSpot JVM、OpenJ9 等,这些实现根据相应的操作系统和架构负责将字节码转化为可执行代码。 优化和性能:JVM 还具有优化功能,能够在运行时对字节码进行优化,以提高程序的性能和效率。 Java 类库:Java 还提供了丰富的类库,这些类库是跨平台的,可以在任何支持 Java 的系统上使用,无需对不同平台进行特定的编码。
通过这些特性和 JVM 的设计,Java 程序可以在不同操作系统和硬件平台上运行,实现了“一次编写到处运行”的目标,这也是 Java 成为一种跨平台开发语言的重要原因之一。
3、写完了之后无论是在Windows上,还是在Mac上面,还是在Linux上面,是都可以运行的,是吗?我们在windows上编写完成,再进行编译后,我们可以拿到对应的操作系统中运行,但,也需要对应操作系统中安装了相应版本的JVM。
4、那你可以谈一下你对 Java 异常的了解吗?比如说它的异常是怎么分类的呀?或者说你在项目中是怎么啊?下面我来说说 Java 异常的分类以及在项目中处理异常的一般方法:
Java 异常分类: 可检查异常(Checked Exception):可检查异常是在编译时强制处理的异常,必须在代码中显式捕获或声明抛出。例如:IOException、SQLException 等。运行时异常(Unchecked Exception):运行时异常是指在运行时期间才会发生的异常,编译器不会强制处理。通常是由程序错误导致的,如空指针异常(NullPointerException)、类型转换异常(ClassCastException)等。错误(Error):错误表示严重的问题,通常由 JVM 运行环境引起,难以恢复。一般情况下不需要捕获处理,如内存溢出(OutOfMemoryError)、栈溢出(StackOverflowError)等。异常处理方式: try-catch块:使用 try-catch 块捕获并处理异常,在 try 块中放置可能发生异常的代码,然后在 catch 块中处理异常情况。throws关键字:在方法声明处使用 throws 关键字声明可能抛出的异常,将异常交给上层调用者处理。finally块:finally 块中的代码总是会执行,无论是否发生异常,通常用于释放资源等清理操作。自定义异常:可以创建自定义异常类来实现业务相关的异常处理,继承 Exception 或 RuntimeException。在项目中的应用: 异常日志记录:捕获异常后记录异常信息到日志,包括异常类型、错误信息、堆栈跟踪等,有助于排查问题。异常处理策略:根据项目需求制定合适的异常处理策略,如重试策略、回滚操作、降级处理等。合理抛出异常:在适当的情况下抛出异常,避免捕获一切异常,保持代码的清晰和规范。统一异常处理:可通过全局异常处理器统一处理异常,提高代码的可维护性和运行稳定性。在项目中,合理处理异常可以提高代码的健壮性和可维护性,避免程序运行时出现意外情况导致系统崩溃。
5、那么你是怎么处理这些异常,并且把这些异常信息返回给前端的?在项目中处理异常并将异常信息返回给前端通常需要遵循一定的做法,以确保系统的稳定性和用户体验。以下是一般的异常处理和异常信息返回给前端的方法:
异常处理和返回给前端的步骤: 捕获异常:在代码中使用 try-catch 块捕获可能抛出的异常,确保异常被捕获。异常处理:根据异常的类型进行相应的处理,可以是记录日志、向用户报告异常、执行特定的恢复操作等。异常信息封装:创建一个统一的异常信息结构,包括异常类型、错误代码、错误消息等,并封装异常信息以便返回给前端。返回异常信息给前端:在发生异常时,通过适当的方式将异常信息返回给前端。可以是通过 HTTP 状态码、JSON 格式的数据返回、自定义错误页面等形式。友好的错误页面或消息:提供用户友好的错误页面或信息,帮助用户理解发生的问题,并提供可能的解决方案。比如我们在Spring Boot项目中的代码示例:
@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { // 创建一个 ErrorResponse 对象,封装异常信息 ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value, e.getMessage); // 记录异常信息到日志 log.error("An error occurred: {}", e.getMessage); // 返回异常信息给前端 return new ResponseEntity(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler(BusiException.class) public ResponseEntity handleException(BusiException e) { // 创建一个 ErrorResponse 对象,封装异常信息 ErrorResponse errorResponse = new ErrorResponse(e.getCode, e.getMessage); // 记录异常信息到日志 log.error("An error occurred: {}", e.getMessage); // 返回异常信息给前端 return new ResponseEntity(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); }}public class ErrorResponse { private int code; private String message; // 构造函数、getter 和 setter 方法}在上面的代码示例中,通过 @RestControllerAdvice 注解创建一个全局异常处理器,在 handleException 方法中处理所有 Exception 类型的异常,封装异常信息到 ErrorResponse 对象,并返回给前端。
建议和注意事项: 细化异常处理:根据具体业务场景细化异常处理,提供恰当的反馈给用户。安全性考虑:避免将敏感信息暴露给前端,确保异常信息的安全性。我们可以通过合理的异常处理和信息返回,可以帮助用户更好地理解系统中出现的问题,并提高用户体验。
6、JSON它本质上它说白了就是会把一个实体类转化成一个 JSON 串,那么你这个通用实体类,你会使用哪些格式去构造构建通用实体类并将其转换为 JSON 格式是常见的操作,特别是在 Web 开发中。以下是一些常用的格式和技巧来构造通用实体类以支持 JSON 转换:
使用的格式和技巧: POJO 类(Plain Old Java Object):创建简单的 Java 类,包含私有字段、公共的 getter 和 setter 方法,用于表示数据模型。无参构造函数:提供一个无参构造函数,以便 JSON 转换库能够实例化对象。属性的访问修饰符:使用私有字段并通过公共的 getter 和 setter 方法来访问属性,符合封装的概念。注解:使用注解如 @JsonProperty、@JsonFormat、@JsonIgnore等来自定义属性的 JSON 序列化和反序列化行为。日期格式化:对日期类型的属性通过 @JsonFormat 注解指定日期格式,确保日期在转换为 JSON 时具有统一的格式。处理关联关系:对于关联关系,可以使用嵌套对象或者在需要的地方使用引用来表示。枚举类型:对枚举类型的处理,可以使用 @JsonEnumDefaultValue 注解提供默认值。null 值处理:对于可能为 null 的属性,可以处理 null 值的显示方式,例如设置默认值或忽略 null 值的序列化。代码示例:
import com.fasterxml.jackson.annotation.JsonFormat;public class User { private Long id; private String name; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date birthDate; // constructors, getter and setter methods}在上面的示例中,User 类是一个简单的实体类,包含 id、name 和 birthDate 属性。通过 @JsonFormat 注解指定了 birthDate 属性的日期格式,以确保在转换为 JSON 时显示正确的日期格式。
注意事项:
在构建通用实体类时,考虑到数据的复杂性和需求,灵活运用不同的注解和技巧来满足系统的需求。保持代码简洁和易读,避免过度使用注解或复杂的逻辑。在处理特殊情况时,例如循环引用、复杂关联关系等,需要额外注意处理方式,以避免序列化和反序列化的问题。我们可以通过合适的实体类的构造和 JSON 格式化,可以提高数据在前端和后端之间的交互效率和一致性。
7、Java的的内存模型,那你可以说一下它的内存模型是什么样的吗?Java的内存模型(Java Memory Model,JMM)是一个抽象的概念,它定义了Java虚拟机(JVM)在计算机内存中的工作方式,包括各种变量的访问规则,以及线程之间的交互方式。JMM关键在于它确保了程序在多线程环境中能够正确执行,通过定义对共享变量的读写规则来保证线程安全。
JMM的关键特性: 可见性:一个线程对共享变量的修改,能够及时地被其他线程看到。JVM通过内存屏障实现。原子性:保证指令不会因为线程调度而中断,即一组操作要么全部完成要么全不完成。有序性:保证程序执行的顺序按照代码的先后顺序执行。Java内存模型是理解并发编程和实现线程安全的关键,它影响了Java中锁、volatile关键字、final关键字、以及线程安全集合等并发工具的设计与实现。例如:
使用volatile关键字可以保证变量的可见性和一定的有序性,但不具备原子性。synchronized关键词和Lock等锁机制用于实现原子性操作,同时也确保了变量操作的可见性和有序性。final关键字可以看作是一种特殊的“锁”,它可以确保对象一旦被构造完成,其被final修饰的字段不可修改。其实,对于我们Java开发者来说,理解Java的内存模型对于编写高效且线程安全的并发程序至关重要。
8、你了解哪些我们会使用本地方法栈的方法?本地方法栈(Native Method Stack)是Java虚拟机中与Java方法调用相关的部分,用于支持调用本地(Native)方法,即由本地语言(如C/C++)编写的方法。当Java程序调用本地方法时,通过本地方法栈来执行本地方法的调用和处理。
一些常见情况下会使用本地方法栈的方法包括: JNI方法调用:- JNI(Java Native Interface)允许调用本地方法,通常涉及使用C或C++编写的本地库。在这种情况下,本地方法栈用于执行Java与本地代码之间的交互。 操作系统功能调用:- 有些Java库可能需要直接调用操作系统的功能,这需要通过本地方法栈来执行这些操作系统调用。 性能优化:- 在某些情况下,通过本地方法实现某些计算密集型操作可以获得更高的性能,因此可以利用本地方法栈来提升程序执行效率。 硬件级别的操作:- 部分Java应用可能需要直接与硬件进行交互(如访问硬件设备、特定硬件功能),这种情况下也会使用本地方法栈。 第三方库的调用:- 有些第三方库可能是用C或C++编写的,并通过Java的JNI机制来实现Java代码与这些库的交互,因此在调用这些库时会涉及本地方法栈。
注意事项:
使用本地方法栈需要谨慎,因为直接调用本地方法会绕过Java的内存管理和安全机制,可能导致内存泄漏或安全漏洞。在使用本地方法栈时,需要确保编写的本地方法是正确的、安全的,以避免对整个应用程序造成不可预料的影响。总的来说,本地方法栈主要用于支持Java程序与本地代码的交互,能够扩展Java程序的功能,提供更广泛的应用场景,但需要注意管理好与Java虚拟机的交互,确保程序的稳定性和安全性。
9、Java 的垃圾回收机制,你可以简单地说一下。Java的垃圾回收机制是Java虚拟机(JVM)自动管理内存的重要组成部分,它负责在运行时自动回收不再使用的内存,避免内存泄漏和提高程序的性能。以下是关于Java垃圾回收的简要介绍:
Java垃圾回收的基本原理: 标记-清除(Mark and Sweep):- JVM通过扫描堆内存中的对象,标记哪些对象是活动的(正在使用的),然后清除那些没有被标记的对象。清除的对象的空间将被释放以供新的对象使用。 标记-整理(Mark and Compact):- 除了标记和清除对象外,还会对存活的对象进行整理,将它们向堆的一端移动,以减少堆内存的碎片化。 分代回收:- 将堆内存分为不同的代(Generation),一般分为新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,Java 8及之前版本)/元空间(Metaspace,Java 8之后版本)。不同代使用不同的垃圾回收算法,根据对象的生命周期来提高回收效率。 停顿时间优化:- 当进行垃圾回收时,会产生一些停顿时间(GC Pause),影响程序的响应性。为了减少停顿时间,JVM引入了一些优化技术,如并发标记清除、并发标记整理等。
垃圾回收算法: 新生代回收:- 一般采用复制算法(Copying Algorithm),将存活的对象从一个区域复制到另一个区域,然后清理出不再使用的区域。 老年代回收:- 一般采用标记-清除算法或标记-整理算法。 永久代/元空间回收:- 永久代用于存储类的元数据信息,在Java 8后被元空间取代。永久代/元空间的回收主要是通过卸载不再使用的类或元数据来实现。
优点和注意事项: 优点:Java的垃圾回收机制可以自动管理内存,减少开发人员手动释放内存的工作,避免内存泄漏和悬挂指针等问题。 注意事项:虽然Java的垃圾回收能够自动处理大部分内存管理工作,但在编写代码时还是需要注意避免创建过多临时对象、合理设计数据结构等,以优化内存的使用和垃圾回收效率。
10、你自学的这两个项目你可以说一个,就是你觉得最有意思,说觉得学习到的最多的一个项目吗?这个是开发性问题,我们对于自己的项目要进行详细的梳理。
梳理出我们在设计过程中的一些难题,最终是怎么解决的,这个过程为我们学到了什么知识以及一些设计技巧。
梳理出我们在实际开发中的一些bug,以及部署到线上环境后出现的问题,是如何解决的,用到了哪些问题排查工具,以及问题排查方法套路。
11、那你是怎么去学习 spring 的AOP、 IOC 这些它的一些特性的?因为我的英语阅读水平还是可以的,所以,我都是看官网,然后再针对AOP和IOC进行案例实战,通过项目案例实战能给我更深层的理解他们的特性。
自己也会看一些博客相关文章,看看自己的理解和他们的理解是否一致,自己也对Spring的AOP和IOC的一些理解和实战写了一些博客。
12、你觉得如果说你后面来我们这边实习的话,那你会从哪些方面开始继续进行学习?这个最好是在面试之前了解一下这家公司的情况,如果能知道他们的技术栈之类的那是更好。
我会抽时间不断巩固基础,如何解决一个功能点该如何设计,以及一些开发中常用的工具。因为任何技术方案都是来自于业务,掌握业务,然后去设计和实现,平时也会抽时间学习前辈们的一些设计方案和设计思路。
13、你可以说一下你最近学的这个并发编程,你有什么感觉比较有意思的一些点?我最近在学并发编程,发现这些技术点都是可以串联起来的。
有意思的点,比如ThreadLocal,我们在多线程环境下通常都会考虑可见性,但是ThreadLocal确实反着来的,他是线程与线程之间是不可见的。
这个问题你可以按照你掌握的比较深的部分聊,不然很容易卡主。
14、怎么理解线程安全的?线程安全是指在多线程环境下,一个方法或数据结构可以在不需要额外同步措施的情况下,依然能够正确地并发访问和操作,不会导致数据错乱、丢失或不一致的现象。
其实个人的理解,线程安全的关键在于保证多个线程同时访问共享资源时的正确性和稳定性。
比如线程安全的重要性: 数据一致性:在多线程环境下,如果不保证线程安全,可能导致数据不一致性,破坏整个程序的正确性。避免竞态条件:竞态条件(Race Condition)可能导致多线程操作相互干扰,影响程序的预期行为。还有就是实现线程安全的方法: 加锁机制:使用锁(如synchronized关键字、ReentrantLock),确保同一时间只有一个线程可以访问共享资源,避免竞态条件。使用线程安全的数据结构:如ConcurrentHashMap、Atomic类等,这些数据结构内部已经实现了线程安全操作,可以减少手动加锁带来的开销。使用不可变对象:不可变对象一定程度上可以减少并发访问带来的问题,因为其状态不可改变。使用ThreadLocal:ThreadLocal可以为每个线程提供独立的变量副本,避免线程间的数据共享问题。多线程环境下容易导致的三个问题: 原子性:确保操作是原子的,不可分割的,要么全部执行成功,要么全部执行失败。可见性:当一个线程修改了共享变量的值,其他线程能立即看到这个修改。有序性:在多线程环境下,程序执行的结果是按照代码的顺序执行的。在实际工作中,对于线程安全,个人觉得有三点 避免共享可变状态:尽量减少共享可变状态,尽量使用不可变对象。保持简单性:使用并发工具时尽量简单明了,避免过度复杂的操作。并发编程测试:进行并发测试以确保程序在多线程环境下的正确性。咱们理解线程安全是多线程编程中至关重要的一部分,它能够提高我们呢代码的健壮性和可靠性,避免因为线程竞争而导致的潜在bug。