1、java有哪些集合?
java集合分三种,List、Set、Map,这三种集合适用于不同的场景
- List:适用于有序,可重复的集合
- ArrayList:数组实现的,常用于查询,因为他不需要移动指针,玩的是数据
LinedList: 链表实现的,常用与增删改查,因为他不需要移动数据,玩的是指针
Vectory: 线程安全的,出现问题会抛出异常需要手动捕获(不常用)
Stack:继承自Vector,实现一个后进先出的堆栈(不常用)
- ArrayList:数组实现的,常用于查询,因为他不需要移动指针,玩的是数据
- Set:适用于不可重复集合
- HashSet:哈希表实现的, 数据无序, 可以放一个Null值,存储单列数据
TreeSet:二叉树实现的,数据自动排序,不允许放null值,存储单列数据
- HashSet:哈希表实现的, 数据无序, 可以放一个Null值,存储单列数据
- Map:适用于键值对的存储
- TreeMap: 二叉树实现的,数据有序,HashTable 与 HashMap无序
HashMap:线程不安全,效率快,适用于单线程操作
HashTable:线程安全,因为底层都加了synchronized关键字来确保线程同步,适用于多线程操作
- TreeMap: 二叉树实现的,数据有序,HashTable 与 HashMap无序
- 通常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多线程和线程池应该怎么实现吗
多线程:
-
继承Thread类
-
实现runnable接口
-
使用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、二叉树的分类
- 按照节点数量分类:
- 满二叉树(Full Binary Tree):每个节点要么是叶子节点,要么有两个子节点。
- 完全二叉树(Complete Binary Tree):除了最后一层外,每一层的节点都是满的,且最后一层的节点尽可能靠左排列。
- 按照节点高度分类:
- 平衡二叉树(Balanced Binary Tree):任意节点的两棵子树的高度差不超过 1。
- AVL树:一种特殊的平衡二叉树,通过旋转操作来保持平衡。
- 红黑树(Red-Black Tree):一种自平衡的二叉查找树,具有以下性质:节点是红色或黑色;根节点是黑色;每个叶子节点(NIL节点,空节点)都是黑色;不能有相邻的两个红色节点;从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
- 按照节点访问顺序分类:
- 二叉查找树(Binary Search Tree,BST):左子树上所有节点的值小于根节点的值,右子树上所有节点的值大于根节点的值。
- 线索二叉树(Threaded Binary Tree):为了加快在中序遍历下的遍历速度,将一些空指针域利用起来,指向该节点在某种遍历次序下的前驱和后继节点,这种指针称为线索。
- 按照节点度数分类:
- 完美二叉树(Perfect Binary Tree):所有非叶子节点都有两个子节点,且所有叶子节点都位于同一层级。
- 斜树(Skewed Tree):所有节点只有左子节点或只有右子节点,也可以是左斜树或右斜树。
- 按照特定应用场景分类:
- 堆(Heap):一种特殊的完全二叉树,用于实现优先队列。
- 哈夫曼树(Huffman Tree):一种用于数据压缩的特殊二叉树,通过频率来构建最优编码树。
10、布隆过滤器
布隆过滤器的原理和特点:
- 位数组(Bit Array):布隆过滤器内部使用一个较长的位数组(或称为位向量),初始时所有位都被置为0。
- 多个哈希函数:布隆过滤器使用多个哈希函数(通常是几个独立的哈希函数),每个哈希函数可以将任意输入映射到位数组中的某个位置。
- 插入操作:当一个元素要加入集合时,通过多个哈希函数计算出多个哈希值,然后将对应位数组的这些位置置为1。
- 查询操作:当查询一个元素是否在集合中时,同样通过多个哈希函数计算出多个哈希值,并检查位数组中对应的位置。如果所有的对应位置都为1,则说明该元素可能在集合中;如果有任意一个位置为0,则说明该元素一定不在集合中。
- 误判率(False Positive Rate):由于哈希冲突和位数组的大小限制,布隆过滤器可能会出现误判,即某个元素不在集合中但布隆过滤器认为存在的情况。误判率取决于哈希函数的数量和位数组的大小。
- 应用场景:布隆过滤器常用于需要快速判断元素是否属于某个集合的场景,如网络爬虫中的URL去重、拼写检查、缓存数据过滤等。
布隆过滤器的实现考虑点:
- 哈希函数选择:应选择独立且高效的哈希函数,以减少冲突和误判率。
- 位数组大小:需要根据预期的插入元素数量和期望的误判率来确定位数组的大小。
- 性能权衡:布隆过滤器的性能受到位数组大小和哈希函数数量的影响,需要在空间和时间效率之间做出权衡。
11、wait()和sleep()有什么区别,wait()会释放锁吗
- wait() 方法:
wait()
方法是Object
类中定义的方法,用于线程间的协调和通信。- 调用
wait()
方法会导致当前线程释放它所持有的对象锁,并进入等待(waiting)状态,直到其他线程调用相同对象上的notify()
或notifyAll()
方法来唤醒它,或者等待时间超时,或者线程被中断。 - 一般与
synchronized
关键字一起使用,确保在调用wait()
前线程必须拥有对象的锁,否则会抛出IllegalMonitorStateException
异常。
- sleep() 方法:
sleep()
方法是Thread
类的静态方法,用于使当前线程进入休眠(sleeping)状态,暂停执行一段时间。- 调用
sleep()
方法不会释放任何锁,线程持有的对象锁在调用sleep()
期间仍然保持。 sleep()
方法通常用于实现线程的暂停执行,不涉及线程间的通信或协调。
区别总结:
- 释放锁:
wait()
方法会释放对象锁,而sleep()
方法不会释放任何锁。 - 使用类别:
wait()
通常用于线程间的等待和通信,而sleep()
用于线程的暂时休眠。 - 调用位置:
wait()
方法在同步代码块或同步方法中调用,而sleep()
方法可以在任何地方调用。
12、乐观锁和悲观锁
悲观锁(Pessimistic Locking):
- 定义:
- 悲观锁认为数据在并发访问时会发生冲突,因此在整个数据操作过程中都会持有锁,防止其他事务同时访问和修改数据。
- 实现方式:
- 在数据库中,悲观锁通常通过数据库的锁机制实现,如行锁、表锁等。
- 在Java中,悲观锁可以通过
synchronized
关键字或ReentrantLock
类实现,确保在修改数据时只有当前线程能访问数据,其他线程必须等待释放锁才能进行访问。
- 应用场景:
- 适用于写操作频繁的场景,例如订单支付、库存管理等,因为悲观锁可以有效地防止数据的并发修改,避免数据不一致性。
乐观锁(Optimistic Locking):
- 定义:
- 乐观锁认为数据在并发情况下一般不会发生冲突,因此在进行数据操作之前不加锁,而是在数据更新时检查是否有其他线程对数据进行了修改。
- 实现方式:
- 在数据库中,乐观锁通常通过版本号(Version)或时间戳(Timestamp)来实现。每次读取数据时,将版本号或时间戳一并读出,更新数据时带上这个版本号或时间戳作为条件进行判断。如果其他线程已经修改了数据,则当前线程的操作会失败,需要进行相应的重试或处理。
- 在Java中,乐观锁可以通过CAS(Compare and Swap)操作实现,如
Atomic
类或者版本号控制来实现。
- 应用场景:
- 适用于读操作频繁、冲突少的场景,例如读取文章、查看商品详情等。因为乐观锁避免了加锁的开销,提高了系统的并发性能。
比较总结:
- 性能:乐观锁的性能通常比悲观锁高,因为它避免了长时间的锁持有。
- 冲突处理:悲观锁直接阻塞其他事务来保护数据,而乐观锁在发生冲突时会进行冲突检测和处理。
- 适用场景:根据业务场景选择合适的锁策略,如高并发读写场景适合乐观锁,而频繁更新的场景适合悲观锁。