2023-08-18

面试题目

Java的基本特性,分别说一下?

  1. 跨平台性/可移植性:主要是依赖虚拟机jvm,只要在不同平台安装虚拟机jvm,Java代码编译成.class字节码,字节码通过jvm按照当前平台编译成本地机器码,所以说Java具有良好的可移植性。
  2. 面向对象:Java一是一门面向对象的编程语言。Java提供了类、封装、继承、多态等面向对象的特性,使得程序结构更加灵活和可维护。
  3. 高性能:Java的编译器将代码编译成字节码(.class文件),然后在运行的时候直接使用字节码转换成本地机器码执行。这种方式保证了较高的执行性能,减少在执行过程中出错。
  4. 多线程:多线程可以带来更高效的执行效率,通过使用多线程和锁的机制,开发者可以实现多任务处理和资源共享,提高程序的效率和响应性。

重载和重写有什么区别?

  1. 重载:指在一个类中,可以有多个同名的方法,但方法的参数类型、参数个数或返回类型必须不同。

    • 不会影响父类的方法
  2. 重写:指在子类重写定义父类已有的同名方法,方法名、方法参数个数和返回类型必须完全相同。

    • 会影响父类中public同名的方法

Conllection接口有哪几个实现内容?

Conllection接口常用的有List接口和Set接口,其中List接口常用的实现类有ArrayList和LinkedList。

List接口的实现类:

  • ArrayList:基于数组的可扩展列表,遍历快,支持快速随机访问元素。
  • LinkedList:基于链表实现的列表,遍历慢,支持快速插入和删除操作。

Set接口的实现类:

  • HashSet:基于哈希表现的无序集合,不允许有重复元素。
  • LinkedHashSet:基于哈希表和链表实现的有序集合,按照插入顺序排序。
  • TreeSet:基于红黑树实现的有序集合,保持元素自然排序或指定比较器排序。

List接口有哪些实现类?

List接口有ArrayList和LinkedList,还有被Java官方标注过时的Vector。

LinkedList有用过吗?

LinkedList是基于链表实现的列表,遍历慢,支持快速插入和删除操作。

因为它是由两个内存块组成,前面内存块存储值,后面一个内存块存储下一个值的内存地址,导致遍历的需要进行多次IO,遍历速度就比较慢。

ArrayList底层是怎么实现的?

ArrayList底层就是数组实现的,一个可自动扩展数组,ArrayList扩容时按照50%增加,性能较好。

当创建一个ArrayList时,默认会分配一个大小为0的数组,当添加元素时,会检查当前数组是否满了,如果已满,则会触发扩容操作。扩容操作会创建一个新的数组,将原来数组中的元素复制到新数组中,并更新ArrayList内部的数组引用。扩容大小一般是增加50%的大小,以此提高动态扩容效率。

由于ArrayList底层是基于数组实现的,它具有以下特点:

  1. 随机访问高效:由于基于数组,ArrayList支持通过索引快速随机访问元素。
  2. 插入和删除元素相对较慢:当插入或删除元素时,需要进行元素的移动和复制操作,这可能涉及到大量数据的移动,导致性能相对较低。
  3. 支持快速访问和修改:可以根据索引快速访问和修改元素。
  4. 不适用于频繁的插入、删除操作:由于需要移动元素,当频繁进行插入和删除操作时,建议使用LinkedList等其他数据结构。

说一下Map?

Map是一个集合接口,用于存储键值对数据,通过键来查询值的功能,并且不允许键重复,存储结构和MongoDB有点像。

Map集合有几个特点:

  1. 键唯一性:Map中的键是唯一的,不允许重复。如果使用相同的健插入新的值,会覆盖掉之前的值
  2. 无序性:Map的键值没有固定的顺序,不会按照插入顺序或者其他规则进行排序
  3. 集合视图:Map提供了获取键集、值集合键值对集的方法,可以方便遍历和操作这些集合
  4. 动态大小:Map的大小是动态调整的,根据添加和删除键值对的操作自动增加或减少

Map还有多个实现类,最常用的是HashMap、TreeMap和LinkedHashMap。

  1. HashMap:使用哈希表作为底层数据结构,具有较快的插入和查找速度,没有固定的顺序
  2. TreeMap:基于红黑树实现,按照键的自然顺序或自定义比较器进行排序
  3. LinkedHashMap:集成HashMap,在HashMap的基础上使用双向链表维护键值对的插入顺序,可以按照插入顺序或访问顺序迭代

HashMap线程安全吗?

HashMap是线程不安全的,它不是为多线程并发操作而设计的。在多线程环境下同时对HashMap进行读写操作可能会导致数据不一致或抛出异常。

多线程使用HashMap方式:可以使用安全的线程Map实现,比如使用ConcurrentHashMap,ConcurrentHashMap使用锁分段技术,将哈希表分成多个小的Segment段,每个Segment只锁定当前操作的部分,以提高并发性能, 这样就可以确保数据的一致性和线程安全性。

HashMap存储过程?

存储数据

  1. 根据键的hashCode()方法计算出键的哈希值(hash)
  2. 根据哈希值通过哈希函数转换成数组索引(index),确定数据在数组中的存储位置
  3. 如果该索引位置为存储其他数据,则直接将键值对存储在该位置
  4. 如果该索引位置已经存储 其他数据(即发生了哈希冲突),则使用链表或红黑树等数据结构来解决冲突,将新的键值对添加到冲突位置的数据结构中

获取数据

  1. 根据要获取的键的hashCode()方法计算出键的哈希值
  2. 根据哈希值通过哈希函数转换成数组索引,确定数据在数组中的位置。
  3. 如果该索引位置上存储了数据,则进行键的比较,如匹配成功,则返回对应的值
  4. 如果该索引位置上没有找到匹配的键,或者该索引位置上没有存储数据(即不存在该键),则返回null

HashMap的底层1.7和1.8有什么区别?

JDK 1.7

JDK1.7之前存储方式采用的是数组+链表的数据结构来解决哈希冲突。

  • 具体来说,就是使用数组存储键值对,每个数组元素称为桶(bucket),每个桶可以存储多个键值对,当发生哈希冲突时,新的键值对会以链表的形式插入到冲突的桶中,这种方式也叫“链表法”。

缺点:当哈希冲突较多时,链表就会变得很长,导致查询效率下降,HashMap的性能就较差。

JDK 1.8

JDK 1.8中的HashMap有针对1.7版本进行优化,主要改进引入了红黑树来代替链表,以提高在桶查询效率。

JDK1.8之后存储方式采用的是数组+链表+红黑树,当链表长度大于8的时候,就会自动转换成红黑树,这种方式成为“树化”。

TCP和UDP的区别?最大区别是什么?

TCP和UDP都是常见的网络传输协议。

TCP是面向连接、可靠的流协议。流是指不间断的数据结构,当使用TCP发送内容过大时,TCP会将内容分割成多个数据块,不间断的发送,以此保证顺序。并且在丢包时可以重发控制,还可以对次序乱掉的分包进行顺序控制。

此外,TCP作为一种面向有连接的协议,只有在确认通信对端存储在才会发送数据,从而可以节约流量的浪费。

TCP一般用于电子邮件、文件传输等

UDP是不可靠传输,UDP不保证传输内容一定到达接收端,它只管发送。因此,如果在传输途中出现丢包,UDP也不负责重发,当包的到达顺序出现乱序时,UDP也没有纠正功能。

UDP一般用于包总量较少的通信,如DNS,视频、音频等多媒体通信、广播通信等

inner join和left join的区别?

  1. Inner Join: 内连接是基于连接条件将两个表中符合条件的记录进行匹配,只返回两个表中共有的数据行。即,只返回左表和右表中连接条件满足的记录。如果某条记录在左表中存在但在右表中不存在,或者在右表中存在但在左表中不存在,都不会被包含在结果中。
  2. Left Join: 左连接是以左表为主,将左表中的所有记录都包括在结果集中,而右表中与左表连接条件不满足的记录则用NULL填充。即,左连接会返回左表中所有的记录,无论在右表中是否有匹配的记录。如果连接条件不满足,右表中对应的字段会用NULL值表示。

正向代理和反向代理的区别?

正向代理最好的例子就是我们翻墙的VPN,我们在客户端使用VPN连接正向代理服务器,客户端请求的服务都由代理服务器代替请求,这样做可以隐藏了真实的客户端。简单来说,就是多个客户端发起请求,都由一个服务器回应。

image-20230905135658095

反向代理最常见的例子就是百度搜索,当我们访问百度搜索时,百度会根据请求的IP寻找最近的缓存服务器来获取资源,并且每一次请求访问到服务器可能都不一样。简单来说就时一对多,一个客户端请求,可能会得到多个服务器的回应。

image-20230905140103816

nginx的upstream说一下

upstream是Nginx的一个模块,可以用来定义一组服务器,用作负载均衡,减少单个服务器的压力。

听过定义不同的配置方式,可以达到想要不同的负载均衡效果,比如有轮训方式、权重方式、ip_hash方式,以及使用第三方模块的方式。

我们一般是在nginx的主配置文件的http下添加一个upstream组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
http {
upstream web_server {
server backend1.example.com;
server backend2.example.com;
...
}

server {
listen 80;
server_name test.ng.com;
location / {
proxy_pass http://web_server;
}
}
}

当然,server块你在子配置文件添加也是可以的。

SpringBoot和传统的Spring有什么区别?

  1. 简化开发配置:传统的Spring框架需要手动配置大量的XML文件,而Spring Boot使用约定大于配置的原则,还提供了自动配置功能。
  2. 内嵌服务器:在传统的Spring框架,没有自带web服务器,需要自行添加web服务器,在Spring Boot中内嵌了Tomcat、Jetty等常用的web服务器,内嵌的好处就是直接将程序打包成jar包,就可以直接运行,不需要依赖额外的应用服务器。
  3. 自动化依赖管理:Spring Boot引入了自动化依赖管理的概念,通过在项目中添加合适的starter依赖,maven就会自动导入所需的依赖项,并进行版本管理,可以避免依赖冲突。
  4. 支持微服务:Spring Boot特别适合构建微服务架构,它提供了对常见的微服务组件(如服务注册与发现、负载均衡等)的集成支持,以及对分布式系统开发的良好支持。而传统的Spring框架更侧重于构建单体应用程序。

Spring框架为什么更侧重于构建单体应用程序?

Spring框架在诞生初期就没有针对微服务进行专门的设计,因此,它更倾向于构建单体应用程序。

  1. 架构风格:传统的Spring框架采用典型的分层架构,将应用程序划分为多个二模块(如控制器。 服务层、持久层),通过依赖注入和面向切面编程等特性来提供解耦、可维护和可测试的代码。这种架构更适用单体程序。
  2. 数据库事务管理:传统的Spring框架通过事务管理功能来支持事务的处理。在单体应用程序中,事务范围通常涵盖整个应用程序的请求处理过程,通过使用声明式事务管理,可以更方便地管理数据库事务。
  3. 部署和扩展:传统Spring框架通常部署在应用服务器上,通过war包进行部署。这种部署方式适用单体应用程序,可以部署整个应用程序作为一个整体。
  4. 依赖关系管理:传统的Spring框架采用XML配置文件或者Java注解来管理应用程序的依赖关系。在单体应用程序中,这种方式可以比较方便地组织和管理各个模块之间的依赖关系。

虽然说Spring框架侧重于单体应用,但并不排斥微服务架构,通过配置和开发,还是可以在Spring框架下构建微服务应用程序的,只是稍微繁琐一些。

SpringBoot的核心注解是什么?

SpringBoot的核心注解是@SpringBootApplication,这是一个组合注解,它包含了@Configuration、@EnableAutoConfiguration、@ComponentScan三个注解。

  1. @Configuration:表示该类为配置类,可以定义一系Bean或者一些特定的配置
  2. @EnableAutoConfiguration:启动自动配置机制,让Spring Boot格局classpath下的依赖自动进行配置。
  3. @ComponentScan:启用组件扫描,自动扫描并注册被@Component、@Server、@Repository、Controller等标识的Bean。

使用@SpringBootApplication注解可以完成核心的配置、自动装配和组件扫描操作,简化了应用程序的配置过程。

控制反转主要作用是什么?

控制反转主要作用是解耦。将对象的创建和依赖关系的管理交给容器来处理,从而解耦应用程序中各个对象之间的依赖关系。

  1. 解耦:通过控制反转,可以将对象之间的依赖关系从代码中接触,使得代码更加简洁,并且减低了对象之间的耦合性。各个对象只需要接口或抽象,而不需要关心具体的实现类。
  2. 可扩展性:可以更加方便的添加、替换或升级对象的实现,而不需要依赖该对象的其他部分代码。只需要在容器中配置新的实现类,然后在需要使用的地方注入相应的实例即可。
  3. 可维护性:对象之间的依赖关系由容器来关系,使得代码更加容易理解与维护。当需要修改某个对象的依赖关系时,只需要修改容器中的配置,而不需要修改大量的代码。
  4. 集中管理:可以将对象的创建和依赖关系的管理集中到容器中。这样可以更好地关系对象的生命周期、资源的释放以及其他与对象的相关操作。
  5. 测试性:由于控制反转将对象之间的依赖关系解耦,我们可以更方便地进行单元测试。可以使用模拟对象或者桩对象代替真实的对象,从而更加方便地进行测试。

依赖注入是什么?

依赖注入(Dependency Injection,简称DI)是一种设计模式,也是控制反转的具体实现方式之一。

通过传递对象的依赖关系来解决对象之间的依赖关系的设计模式。它能够减低耦合度、提高可测试性和可替换性。

在Spring框架中,依赖注入是通过容器来实现的,Spring容器负责创建和管理对象,并根据配置信息将对象的依赖关系自动注入到相应的位置。

创建Bean的创建过程,生命周期是什么?

Bean的声明周期包括创建、依赖注入、初始化、使用和销毁过程。

  1. 定义Bean:通过XML配置文件或者用注解(如@Component等)的方式,将一个类表示为一个Bean,并指定该Bean的相关属性和依赖关系。
  2. 加载配置:Spring容器负责加载配置文件,解析配置信息,并根据配置信息创建响应的BeanDefintion对象。(BeanDefintion对象包含了Bean的详细信息,如类名、属性值、依赖关系等)
  3. 创建实例:当需要获取某个Bean时,Spring框架会根据BeanDefintion中的信息,使用响应的测试(如构造方法、工厂方法等)来创建Bean的实例。
  4. 设置属性:在创建Bean实例后,Spring容器会自动将配置文件中指定的属性值或者依赖对象注入到Bean实例中,完成依赖注入的过程。
  5. Aware回调:如果Bean实现了相应的Aware接口,Spring容器会自动调用其对应的回调方法,以提供一些额外的容器信息给Bean使用。
  6. 初始化方法:如果Bean配置了初始化方法,Spring容器会在依赖注入完成后,调用Bean的初始化方法,完成一些初始化逻辑。
  7. BeanPostProcessor处理:如果配置了BeanPostProcessor(后置处理器),Spring容器会依次调用所有注册的BeanPostProcessor的方法,在Bean初始化前后进行一些额外的处理。
  8. 使用Bean:Bean已经被成功创建和初始化了,可以在容器中获取该Bea n了。
  9. 销毁方法:当容器关闭或者销毁某个Bean时,如果配置了销毁方法(在XML使用<destory-method>或使用@ProDestory注解),Spring容器会在适当的时候调用Bean的销毁方法。

Bean循环依赖底层是怎么解决的?

三级缓存

说一下AOP?

AOP又称面向切面编程,是一种编程范式,用于横切关注点从主要业务逻辑中分离出来。在一些特殊业务中,比如日志记录、权限控制等,使用AOP可以将关注点从业务中抽取出来,形成一个独立的模块,这样可以降低耦合度。

AOP有几个核心的概念和组件,如切面、切入点、连接点、通知。

  1. 切面:定义了再那些连接点上执行什么样的操作,一个切面可以包含多个通知。
  2. 切入点:切入点定义了那些连接点会被匹配到,使用表达式来指定范围。
  3. 连接点:连接点是在应用程序中可以插入切面的点。比如方法调用、异常抛出等都是连接点。
  4. 通知:通知定义再连接点上要执行的操作,包括在切面在特定连接点上执行的代码逻辑。

MySQL中什么是索引?

索引是一种数据结构,用于提高数据库表的查询性能。类似书本的目录,可以快速定位想要查找的数据。

创建索引时会额外创建一个数据结构,用于存储索引列的值和指向实际数据位置的指针,这样在查询时就可以直接使用索引进行搜索,就不需要扫描整张表,当然创建新的数据结构也会占用一定的存储空间,并且在数据更新时维护索引的更新,可能会影响到插入、更新和删除操作的性能。

说一下B+树的结构?

B+树是B树基础上的一种优化。在B+树中,非叶子节点上仅存储键值,不存储数据,而所有数据记录均存储在叶子节点上,并且数据是按照顺序排列,并且B+数中各个数据页之间是通过双向列表连接的。

并且B+树的高度是比较低的,这样可以减少磁盘I/O, 提高检索效率。

同时子节点是按照顺序排列的,对于范围查询非常高效。

image-20230906200635599

在B+树中,创建索引和不创建索引在结构上有什么区别?

创建索引时:额外的索引数据结构被构建与原始数据并列存在。这些索引包含需要建立索引的列的值及其对应的指针或关联数据。索引数据结构按照特定的算法来组织和排序索引值,以支持高效的查询操作。

不创建索引时:只有原始的数据结构存在,没有额外的索引数据结构。查询时需要逐个扫描整个数据结构来寻找满足条件的数据,这可能导致查询操作的开销增加,尤其是在数据量较大的情况下。

重点:索引的存在可以大大提高搜索和范围查询的效率。通过索引,查询操作可以直接定位到满足条件的数据所在的位置,而不需要逐个遍历整个数据结构。这样可以减少磁盘I/O操作,从而提高查询性能。

虽然提高了检索效率,但创建索引也会增加存储空间的开销,索引数据结构需要占用额外的存储空间,特别是在索引列的技术较高或者数据较大的情况下,索引会占用较多的存储空间,属于是拿空间换时间了。

说一下回表的过程?

回表是指在使用非覆盖索引进行查询时,需要通过回表操作获取完整的数据行。当数据的列不在索引中时,MySQL无法直接从索引中获取所需的数据,而是需要通过回表操作主键索引或者聚簇索引中查找完整的数据行。

覆盖索引:需要查询的字段都在索引列中。比如select id from tb_user where age = 18,该查询语句使用age条件,但id是主键,当走age索引查询数据时,age数据结构会包含id的,也就可以直接返回了,就不需要进行回表操作了。

索引在什么情况下失效?

索引失效有很多种情况,比如

  1. 不使用索引字段:当查询条件中不包含索引字段时,MySQL就无法使用索引进行快速查找了,而是需要进行全表扫描了。
  2. 对索引字段使用函数操作:在查询条件中对索引字段进行了函数操作,索引就会失效。
  3. 使用不等于操作符(!=、< >和not in):不等于操作符不属于范围查询,需要进行全表扫描,索引也自然失效了。
  4. 数据量太小:数据表的记录数据太小的话,使用索引反而会导致性能下降,因为MySQL需要进行额外的索引检索操作。
  5. 索引列存在大量重复值:存在大量重复值,索引的选择性就较低了,MySQL可能会放弃使用索引而走全表扫描。
  6. 组合索引没有使用索引前缀:如果有使用组合索引,但在使用索引时没有使用组合索引的前缀部分,则索引就会失效。
  7. 数据表使用不合适的存储引擎:不同的存储引擎对索引支持程度不同,如果选择的存储引擎不适合使用索引,那索引也会失效。

聚合函数是什么?

在MySQL中,聚合函数是可以对数据进行计算和统计的函数。如count()、sum()、max()、min()等。一般配合group by进行分组计算。

Redis缓存为什么这么快?

  1. Redis缓存是基于内存的缓存系统,将数据存储在内存中,而不是存储在磁盘,相比磁盘,内存读写的速度快多了。
  2. 简单的数据结构:并且Redis的存储数据结构也非常简单,Redis支持多种简单的数据结构,日字符串、列表、集合、哈希表等,这些数据结构都是基于内存的,操作简单高效。
  3. 单线程模型:Redis采用单线程模型,避免了多线程资源竞争问题,可以简化并发访问的处理,提高系统的响应速度。
  4. 异步操作:Redis还支持批量操作和异步操作,通过将请求批量发送给服务器,然后一步处理返回结果,可以提高系统的吞吐量和响应速度。
  5. 持久化支持:除了内存存储,Redis还支持数据持久化功能,可以将内存中的数据定期或者特定时间点写入磁盘,以保证数据的持久性。

Redis的五种数据类型是什么?

string

此类型和memcache相似,作为常规的key-value缓存应用。
例如微博数、粉丝数等
:一个键最大能存储512MB

hash

redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象(应为对象可能会包含很多属性)
常用命令:hget hset hgetall

list

list列表是简单的字符串列表,按照插入顺序排序(内部实现为LinkedList),可以选择将一个链表插入到头部或尾部
常用命令 :lpush(添加左边元素),rpush,lpop(移除左边第一个元素),rpop,lrange(获取列表片段,LRANGE key start stop)等。
应用场景:Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现。

set

案例:在微博中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注共同喜好二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

zset

常用命令:zadd,zrange
实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,跳跃表按score从小到大保存所有集合元素。使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。时间复杂度与红黑树相同,增加、删除的操作较为简单。
应用场景:排行榜

实际需求

问:更新数据时,先删除Redis缓存,再更新数据库还是先更新数据库有再删除缓存?原因是什么?

JAVA 中的 volatile 关键字是什么

volatile是一个关键字吗,用于声明变量,主要作用是保证多个线程之间对该变量的可见性和有序性。

实现多线程有多少种方式?

常见的方式有四种:

  1. 继承Thread类并重写run方法可以创建线程,然后在调用的时候new一个对象并启动start方法即可,但不能获取返回值
  2. 实现Runnable接口,重写run方法定义需要执行多线程的任务
  3. 实现Callable接口并实现call方法来创建线程,并且可以获取返回值和抛出异常
  4. 使用线程池创建线程

多线程那种方法那种可以获取结果?

可以使用Callable和Future来获取返回结果

启动线程为什么要使用start( )方法

创建线程池有哪几个参数?

如果在访问接口访问很慢,怎么排查问题?

  1. 确定问题的现象和范围:浏览器打开F12,观察请求接口是否只有单个或者批量出现问题,观察请求返回状态码是多少,如200、403、404等
  2. 确定客户端和服务端是否连通:可以使用ping命令进行测试,或者用curl其他工具测试
  3. 检测服务端的负载情况:可以看到CPU和内存占用的情况,看看是否有占满,如果有,可以释放一些
  4. 检测接口代码:检测接口代码是否有问题,如死循环之类
  5. 观察日志:查看请求日志和一些相关日志排查问题

冒泡排序的思想是什么?

冒泡排序就如其名,首先是第一第二个值作比较,最大的值往后冒泡,然后第二第三个值继续作比较,最大的值往后冒泡,直至第一轮比较完毕,然后从第二第三个值开始作比较,最大的往后冒泡,如此重复多轮比较。

快速排序是什么?

为什么选择Java领域?

我大学学的主要是Java开发为主,随着深入学习,我发现我对这门语言还挺喜欢,同时学校也有培养计划,对我本身的学习也是一种优势,因此就选择了Java这条路。

接触了新技术,你会怎么下手去学习?

如果是我的话,我会先在知乎搜一下相关技术的介绍,先大概了解一下技术内容,然后再去掘金类似的技术平台寻找更深入学习。

本文章来源于我的博客:https://blog.hikki.site