后端实习面试题(二)

1、java有哪些集合?

java集合分三种,List、Set、Map,这三种集合适用于不同的场景

  • List:适用于有序,可重复的集合
    • ArrayList:数组实现的,常用于查询,因为他不需要移动指针,玩的是数据
      LinedList: 链表实现的,常用与增删改查,因为他不需要移动数据,玩的是指针
      Vectory: 线程安全的,出现问题会抛出异常需要手动捕获(不常用)
      Stack:继承自Vector,实现一个后进先出的堆栈(不常用)
  • Set:适用于不可重复集合
    • HashSet:哈希表实现的, 数据无序, 可以放一个Null值,存储单列数据
      TreeSet:二叉树实现的,数据自动排序,不允许放null值,存储单列数据
  • Map:适用于键值对的存储
    • TreeMap: 二叉树实现的,数据有序,HashTable 与 HashMap无序
      HashMap:线程不安全,效率快,适用于单线程操作
      HashTable:线程安全,因为底层都加了synchronized关键字来确保线程同步,适用于多线程操作
  • 通常List与Map最为常用

2、 java基本数据类型

  • 6种数字类型
    • 4种整数型:byte、short、int、long
    • 2种浮点型:float、double
  • 1种字符类型:char
  • 1种布尔型:boolean

3、http和https有什么区别,为什么说https更安全

HTTPS是在HTTP上建立SSL加密层,并对传输数据进行加密,是HTTP协议的安全版。

4、可以说一下java多线程和线程池应该怎么实现吗

多线程:

  1. 继承Thread类

  2. 实现runnable接口

  3. 使用Executor框架

线程池:

流程:

提交任务->核心线程数(未满)->创建核心线程执行

提交任务->核心线程数(已满)->进入工作队列(未满)->任务入队

提交任务->核心线程数(已满)->进入工作队列(已满)->最大线程数(未达)->创建非核心线程执行

提交任务->核心线程数(已满)->进入工作队列(已满)->最大线程数(已达)->根据饱和策略处理任务

作用:使用了 ExecutorService 来创建一个线程池,然后通过调用 submit 方法来提交任务给线程池执行。这种方式更加高效,可以复用线程,并且可以通过配置来限制同时运行的线程数量。

5、请求响应拦截器怎么实现?

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  });

6、 有考虑过断点续传吗?就是突然网络断开,上传或者下载应该继续下载,而不是重新下载?

断点续传

断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分

每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度

一般实现方式有两种:

服务器端返回,告知从哪开始

浏览器端自行处理

上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可

7、介绍一下数组

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

8、数组和链表的区别

比较项 数组 链表
逻辑结构 (1)数组在内存中连续; (2)使用数组之前,必须事先固定数组长度,不支持动态改变数组大小;(3) 数组元素增加时,有可能会数组越界;(4) 数组元素减少时,会造成内存浪费;(5)数组增删时需要移动其它元素 (1)链表采用动态内存分配的方式,在内存中不连续 (2)支持动态增加或者删除元素 (3)需要时可以使用malloc或者new来申请内存,不用时使用free或者delete来释放内存
内存结构 数组从栈上分配内存,使用方便,但是自由度小 链表从堆上分配内存,自由度大,但是要注意内存泄漏
访问效率 数组在内存中顺序存储,可通过下标访问,访问效率高 链表访问效率低,如果想要访问某个元素,需要从头遍历
越界问题 数组的大小是固定的,所以存在访问越界的风险 只要可以申请得到链表空间,链表就无越界风险

9、二叉树的分类

  1. 按照节点数量分类
    • 满二叉树(Full Binary Tree):每个节点要么是叶子节点,要么有两个子节点。
    • 完全二叉树(Complete Binary Tree):除了最后一层外,每一层的节点都是满的,且最后一层的节点尽可能靠左排列。
  2. 按照节点高度分类
    • 平衡二叉树(Balanced Binary Tree):任意节点的两棵子树的高度差不超过 1。
    • AVL树:一种特殊的平衡二叉树,通过旋转操作来保持平衡。
    • 红黑树(Red-Black Tree):一种自平衡的二叉查找树,具有以下性质:节点是红色或黑色;根节点是黑色;每个叶子节点(NIL节点,空节点)都是黑色;不能有相邻的两个红色节点;从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
  3. 按照节点访问顺序分类
    • 二叉查找树(Binary Search Tree,BST):左子树上所有节点的值小于根节点的值,右子树上所有节点的值大于根节点的值。
    • 线索二叉树(Threaded Binary Tree):为了加快在中序遍历下的遍历速度,将一些空指针域利用起来,指向该节点在某种遍历次序下的前驱和后继节点,这种指针称为线索。
  4. 按照节点度数分类
    • 完美二叉树(Perfect Binary Tree):所有非叶子节点都有两个子节点,且所有叶子节点都位于同一层级。
    • 斜树(Skewed Tree):所有节点只有左子节点或只有右子节点,也可以是左斜树或右斜树。
  5. 按照特定应用场景分类
    • 堆(Heap):一种特殊的完全二叉树,用于实现优先队列。
    • 哈夫曼树(Huffman Tree):一种用于数据压缩的特殊二叉树,通过频率来构建最优编码树。

10、布隆过滤器

布隆过滤器的原理和特点:
  1. 位数组(Bit Array):布隆过滤器内部使用一个较长的位数组(或称为位向量),初始时所有位都被置为0。
  2. 多个哈希函数:布隆过滤器使用多个哈希函数(通常是几个独立的哈希函数),每个哈希函数可以将任意输入映射到位数组中的某个位置。
  3. 插入操作:当一个元素要加入集合时,通过多个哈希函数计算出多个哈希值,然后将对应位数组的这些位置置为1。
  4. 查询操作:当查询一个元素是否在集合中时,同样通过多个哈希函数计算出多个哈希值,并检查位数组中对应的位置。如果所有的对应位置都为1,则说明该元素可能在集合中;如果有任意一个位置为0,则说明该元素一定不在集合中。
  5. 误判率(False Positive Rate):由于哈希冲突和位数组的大小限制,布隆过滤器可能会出现误判,即某个元素不在集合中但布隆过滤器认为存在的情况。误判率取决于哈希函数的数量和位数组的大小。
  6. 应用场景:布隆过滤器常用于需要快速判断元素是否属于某个集合的场景,如网络爬虫中的URL去重、拼写检查、缓存数据过滤等。
布隆过滤器的实现考虑点:
  • 哈希函数选择:应选择独立且高效的哈希函数,以减少冲突和误判率。
  • 位数组大小:需要根据预期的插入元素数量和期望的误判率来确定位数组的大小。
  • 性能权衡:布隆过滤器的性能受到位数组大小和哈希函数数量的影响,需要在空间和时间效率之间做出权衡。

11、wait()和sleep()有什么区别,wait()会释放锁吗

  1. wait() 方法
    • wait() 方法是 Object 类中定义的方法,用于线程间的协调和通信。
    • 调用 wait() 方法会导致当前线程释放它所持有的对象锁,并进入等待(waiting)状态,直到其他线程调用相同对象上的 notify()notifyAll() 方法来唤醒它,或者等待时间超时,或者线程被中断。
    • 一般与 synchronized 关键字一起使用,确保在调用 wait() 前线程必须拥有对象的锁,否则会抛出 IllegalMonitorStateException 异常。
  2. sleep() 方法
    • sleep() 方法是 Thread 类的静态方法,用于使当前线程进入休眠(sleeping)状态,暂停执行一段时间。
    • 调用 sleep() 方法不会释放任何锁,线程持有的对象锁在调用 sleep() 期间仍然保持。
    • sleep() 方法通常用于实现线程的暂停执行,不涉及线程间的通信或协调。
区别总结:
  • 释放锁: wait() 方法会释放对象锁,而 sleep() 方法不会释放任何锁。
  • 使用类别: wait() 通常用于线程间的等待和通信,而 sleep() 用于线程的暂时休眠。
  • 调用位置: wait() 方法在同步代码块或同步方法中调用,而 sleep() 方法可以在任何地方调用。

12、乐观锁和悲观锁

悲观锁(Pessimistic Locking):
  1. 定义
    • 悲观锁认为数据在并发访问时会发生冲突,因此在整个数据操作过程中都会持有锁,防止其他事务同时访问和修改数据。
  2. 实现方式
    • 在数据库中,悲观锁通常通过数据库的锁机制实现,如行锁、表锁等。
    • 在Java中,悲观锁可以通过 synchronized 关键字或 ReentrantLock 类实现,确保在修改数据时只有当前线程能访问数据,其他线程必须等待释放锁才能进行访问。
  3. 应用场景
    • 适用于写操作频繁的场景,例如订单支付、库存管理等,因为悲观锁可以有效地防止数据的并发修改,避免数据不一致性。
乐观锁(Optimistic Locking):
  1. 定义
    • 乐观锁认为数据在并发情况下一般不会发生冲突,因此在进行数据操作之前不加锁,而是在数据更新时检查是否有其他线程对数据进行了修改。
  2. 实现方式
    • 在数据库中,乐观锁通常通过版本号(Version)或时间戳(Timestamp)来实现。每次读取数据时,将版本号或时间戳一并读出,更新数据时带上这个版本号或时间戳作为条件进行判断。如果其他线程已经修改了数据,则当前线程的操作会失败,需要进行相应的重试或处理。
    • 在Java中,乐观锁可以通过CAS(Compare and Swap)操作实现,如 Atomic 类或者版本号控制来实现。
  3. 应用场景
    • 适用于读操作频繁、冲突少的场景,例如读取文章、查看商品详情等。因为乐观锁避免了加锁的开销,提高了系统的并发性能。
比较总结:
  • 性能:乐观锁的性能通常比悲观锁高,因为它避免了长时间的锁持有。
  • 冲突处理:悲观锁直接阻塞其他事务来保护数据,而乐观锁在发生冲突时会进行冲突检测和处理。
  • 适用场景:根据业务场景选择合适的锁策略,如高并发读写场景适合乐观锁,而频繁更新的场景适合悲观锁。

你可能感兴趣的:(面试,java,后端)