每日一句
你做不到的,总有人能做到。所以,凭什么是你?
每日一刷
考试时间 1h
Java
1. 为什么java是要求是只能单一继承,却可以实现多个接口?说一下见解?
抽象类是对事物的一种抽象,描述的是某一类特性的事物。表示 这个对象是什么。(is-a关系——强调所属关系),接口是对行为功能的抽象,描述是否具备某种行为特征。表示 这个对象能做什么。(has-a关系——强调功能实现)
抽象类本质上是为了继承而存在,为子类提供一个公共特性的通用模板,是子类的抽象。接口提供一组抽象方法的集合,供子类实现。
2. HashMap与ConcurrentHashMap的区别,ConcurrentHashMap是如何保证线程安全的?
HashMap就是最基础最常用的一种Map,它无序,以散列表(数组+链表/红黑树)的方式进行存储,存储内容是键值对映射。是一种非同步的容器类,故它的线程不安全。
ConcurrentHashMap通过使用分离锁,只针对某(键的hash值对应的)具体的Segment(而不是整个ConcurrentHashMap。因为插入键值对的操作只在这个Segment包含的桶中完成,因此不需要锁定整个ConcurrentHashMap。此时其他写进程对另外15个Segment加锁并不会因为对当前这个Segment加锁而阻塞。同时所有读线程几乎不会因本线程加锁而阻塞)进行加锁,同时允许多线程访问其他未上锁Segment。
相比较于HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。
3. 介绍一下volatile 的的工作原理,为什么不支持原子性
对于非volatile变量进行读写时,每个线程先从主存拷贝变量到线程缓存中,执行完操作再保存到主存中。需要进行load/save操作。
而volatile变量保证每次读写变量都是不经过缓存而是直接从内存读写数据。省去了load/save操作。volatile变量不会将对该变量的操作与其他内存操作一起重排序,能及时更新到主存;且因该变量存储在主存上,所以总会返回最新写入的值。
Volatie不支持原子性的原因?举个栗子:
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
4. 介绍一下类加载机制?
一句话:把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
类的生命周期包括:加载,验证,准备,解析,初始化,使用和卸载(可结合这七个步骤详细说一下,这一块不是很重要,不展开)
其中类的加载由类加载器完成,不同的类加载器负责加载不同的类。主要分为两类。
启动类加载器(Bootstrap ClassLoader): 由C++语言实现(针对HotSpot),负责将存放在lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中,即负责加载Java的核心类。
其他类加载器: 由Java语言实现,继承自抽象类ClassLoader。如:扩展类加载器(Extension ClassLoader): 负责加载libext目录或java.ext.dirs系统变量指定的路径中的所有类库,即负责加载Java扩展的核心类之外的类。应用程序类加载器(Application ClassLoader): 负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器,通过ClassLoader.getSystemClassLoader()方法直接获取。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
以上2大类,3小类类加载器基本上负责了所有Java类的加载。类加载器的工作流程按照双亲委派模型去执行:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
这样的好处是不同层次的类加载器具有不同优先级,比如所有Java对象的超级父类java.lang.Object,位于rt.jar,无论哪个类加载器加载该类,最终都是由启动类加载器进行加载,保证安全。即使用户自己编写一个java.lang.Object类并放入程序中,虽能正常编译,但不会被加载运行,保证不会出现混乱。
5. 如何判断对象是否会被垃圾回收机制处理掉?描述JVM中一次完整的GC过程?
采用可达性分析算法判断对象是否会被回收
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC Root开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,可达的对象都是存活的。当所有的引用节点寻找完毕之后,剩余的不可达节点则被认为是没有被引用到的节点,即无用的节点(GC Root 不可达对象),无用的节点将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终导致内存溢出。为了让程序员更专注于代码的实现,而不用过多考虑内存释放的问题(内存的释放由系统自动识别完成)。Java语言中有了自动的垃圾回收机制,即GC回收机制。
GC回收算法采用的是分代算法
分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收(GC),以便提高回收效率。
Young 年轻代(复制算法)
由于年轻代垃圾对象较多,复制的对象较少,因此复制算法效率较高。年轻代划分为一块较大的Eden区和两块较小的Survivor区(Survivor_From、Survivor_To)
几乎所有新生成的对象首先是放在Eden区间。当Eden区间内存不足时,会发起一次GC。
在GC开始的时候,对象只会存在于Eden区和Survivor_From区,Survivor_To区是空的。
紧接着进行GC,Eden区所有存活对象都会复制到Survivor_To区,Survivor_From区中仍存活的对象根据他们的年龄决定去向,若年龄达到年龄阀值(MaxTenuringThreshold)的对象会移动到老年代,没有到达阀值的对象会复制到Survivor_To区。
清空Eden区和Survivor_From区。然后交换Survivor_To与Survivor_From区。使存活的对象均位于Survivor_From区域,Survivor_To区为空。
GC会一直重复上述过程,直到Survivor_To区被填满(Survivor_To区不足以存放Eden与Survivor_From区中存活的对象时),此时所有存活的对象移动到老年代。 - Tenured 老年代(标记-清除/压缩法)
由于老年代垃圾对象较少(对象存活率较高),因此适合使用标记-清除/压缩法。
当老年代内存满时触发GC。
6. 在项目中使用反射多吗,都用来做什么?如何看待多数人认为反射效率低的问题?
- Java反射机制定义
Java 反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法;对于任意一个对象都能够调用它的任意一个属性和方法。这种在运行时动态的获取类信息以及动态调用对象的方法的功能称为Java 的反射机制。 - Java 反射机制的功能
- 在运行时判断任意一个对象所属的类。
- 在运行时构造任意一个类的对象。
- 在运行时判断任意一个类所具有的成员变量和方法。
- 在运行时调用任意一个对象的方法。
- 生成动态代理。
- Java 反射机制的应用场景
- 逆向代码 ,例如反编译
- 与注解相结合的框架 例如Retrofit
- 单纯的反射机制应用框架 例如EventBus
- 动态生成类框架 例如Gson
- 导致反射效率慢的因素及解决:
- 获取Class对象时使用Class.forName效率低,耗时。把Class.forName返回的Class对象缓存起来,下一次使用的时候直接从缓存里面获取,这样就极大的提高了获取Class的效率。
- 同理,在项目启动时将反射需要的相关配置和数据(Field、Method、Constructor等)加载进内存,在运行时直接从缓存中取这些元数据进行反射操作。
- 调用method.setAccessible(true)
- jdk在设置获取字段,调用方法的时候会执行安全访问检查,而此类操作会比较耗时,所以通过setAccessible(true)的方式可以关闭安全检查,从而提升反射效率。
- 采用高性能的反射工具包,如ReflectASM。
- 使用高版本JDK,提高反射性能。
- 反射效率慢,但速度也是可以接受的。所以对于反射不应该因为速度慢而对其"望而却步"。
7. 在项目中使用过设计模式吗,如何使用?介绍一下代理模式/观察者/构建者模式?
设计模式 | 菜鸟教程
计算机网络
1. 介绍一下三次握手和四次挥手的过程?
-
三次握手
第一次握手:由Client发出请求连接数据包: SYN=1 ACK=0 seq=x(TCP规定SYN=1时不能携带数据,但要消耗一个序号,因此声明自己的序号是 seq=x)此时Client进入SYN-SENT(同步已发送)状态,等待Server确认;
第二次握手:Server收到请求报文后,如果统一建立连接,则向A发送连接确认报文,即 SYN=1 ACK=1 seq=y,ack=x+1,此时Server进入SYN-RCVD(同步收到)状态;
第三次握手:Client收到Server的确认(SYN+ACK)后,向Server发出确认报文段,即 ACK=1,seq=x+1, ack=y+1,TCP连接已经建立,Client进入ESTABLISHED(已建立连接)状态;
Server收到Client的确认后,也进入ESTABLISHED状态,完成三次握手;此时Client和Server可以开始传输数据。
-
四次挥手
第一次挥手:当Client发起终止连接请求的时候,会发送一个(FIN为1,seq=u)的没有数据的报文,这时Client停止发送数据(但仍可以接受数据) ,进入FIN_WAIT1(终止等待1)状态,等待Server确认
第二次挥手:Server收到连接释放报文后会给Client一个确认报文段(ACK=1,ack=u+1,seq=v), 进入CLOSE-WAIT(关闭等待)状态
Client收到Server的确认后进入FIN_WAIT2状态,等待Server请求释放连接,Server仍可向Client发送数据。
第三次挥手:Server数据发送完成后,向Client发起请求连接释放报文(FIN=1,ACK=1,seq=w,ack = u+1),Server进入LAST-ACK(最后确认)状态,等待Client确认
第四次挥手:Client收到连接释放报文段后,回复一个确认报文段(ACK=1,seq=u+1,ack=w+1),进入 TIME_WAIT(时间等待) 状态,Server收到后进入CLOSED(连接关闭)状态。经过等待2MSL 时间(最大报文生存时间),Client进入CLOSED状态。
2. 介绍一下TCP和UDP的区别?
协议
TCP协议
UDP协议
简介
面向连接的、可靠的传输层协议。传输的数据无差错、不丢失、不重复、按序到达(有流量控制、拥塞控制、提供全双工通信)
无连接的、不可靠的传输层协议。尽最大努力交付,不保证可靠性
连接
面向连接(发送数据前三次握手建立连接,发送结束四次挥手释放连接)
无连接的
传输数据
面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块)
面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部)
交互
点对点(一对一)
一对一,一对多,多对多
大小
TCP首部(开销20字节,包括源端口号、目的端口号等以及确认ACK、同步SYN、终止FIN等用于连接建立与数据传输)+TCP数据部分
UDP首部开销8字节(包括源端口号、目的端口号等)+UDP数据部分
适用情景
TCP用于在传输层有必要实现可靠传输的情景
UDP主要用于那些对高速传输和实时性有较高要求的通信或广播通信:1.包总量较少的通信(DNS、SNMP等)2.视频、音频等多媒体通信(即时通信)3.限定于LAN等特定网络中的应用通信4.广播通信(广播、多播)
传输
可靠传输:
(1)传输信道无差错,保证传输数据正确;
(2)不管发送方以多快的速度发送数据,接收方总是来得及处理收到的数据;
可靠性原理:
(1)使用三次握手建立TCP连接,四次握手释放TCP连接,保证建立的传输信道是可靠的。
(2)使用连续ARQ协议(超时自动重传:如果一个已经发送的报文段在超时时间内没没有收到确认,那么就重传这个报文段)来保证数据传输的正确性,
(3)使用滑动窗口协议来保证接收方能够及时处理所接收到的数据,进行流量控制
(4)使用慢开始、拥塞避免、快重传和快恢复来进行拥塞控制,避免网络拥塞
不可靠传输:
UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务
并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。
即使是出现网络拥堵的情况,UDP也无法进行流量控制等避免网络拥塞行为。
此外传输途中出现丢包,UDP也不负责重发(发送后不管其是否会到达接收方)。甚至当包的到达顺序出现乱序也没有纠正的功能。
如果需要以上的细节控制,不得不交由采用UDP的应用程序去处理。
协议
FTP、HTTP、POP3、TELNET…
SMTP(网络管理)、DNS(域名转换)、TFTP(文件传输)、NFS(远程文件服务器)、DHCP…
项目*3
1. 介绍一下车辆监控系统的实现?以及针对项目询问…
略
2. vue-router实现的原理?
vue-router 有 3 种路由模式:hash、history、abstract
- hash:使用 URL hash值作为路由
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。hash 路由模式的实现主要是基于下面几个特性:- URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送
- hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换
- 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
- 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。
- history:使用 HTML5 History API 和服务器配置
history 模式是基于 HTML5 的 History API 来实现的。其中做最主要的 API 有以下两个:history.pushState() 和 history.replaceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
history 路由模式的实现主要基于存在下面几个特性: - pushState 和 repalceState 两个 API 来操作实现 URL 的变化
- 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染)
- history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)
- abstract:支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式
3. 了解React吗,React和Vue的区别?
??:面试官:谈谈Vue和React的区别?
- 相同点:
- 数据驱动视图
- 组件化
- Virtual DOM
- 不同点:
- 核心思想不同
vue的主要特点:灵活易用的渐进式框架,进行数据拦截/代理,它对侦测数据的变化更敏感、更精确。React推崇函数式编程(纯组件),数据不可变以及单向数据流,当然需要双向的地方也可以手动实现, 比如借助onChange和setState来实现。
由于两者核心思想的不同,所以导致Vue和React在后续设计产生了许多的差异。 - 组件写法差异
React推荐的做法是JSX + inline style, 也就是把 HTML 和 CSS 全都写进 JavaScript 中,即 all in js; Vue 推荐的做法是 template 的单文件组件格式(简单易懂,从传统前端转过来易于理解),即 html,css,JS 写在同一个文件(vue也支持JSX写法)
这个差异一定程度上也是由于二者核心思想不同而导致的。 - diff算法不同
- 核心思想不同
算法*3
1. 求一个二叉树中任意两个节点间的最大距离(LeetCode-543)
??:https://leetcode-cn.com/problems/diameter-of-binary-tree/
题目描述:
给定一棵二叉树,计算这课二叉树的直径长度,即为二叉树任意两个节点间的最长路径。比如:
这棵二叉树的最长路径为3。
解题思路:
使用递归进行求解,每次递归的过程中,先求出以某个节点为树根的二叉树的左子树的最长深度left_len、右子树的最长深度right_len,并在递归函数中用一个变量diameter来保存任意两个节点间的最长路径。在求出左子树的最长深度left_len和右子树的最长深度right_len之后,就可以求出以该节点为根的二叉树的最长路径diameter。具体代码如下:
int diameterOfBinaryTree(TreeNode* root) {
if(root==nullptr)
return 0;
int diameter = 0; //保存直径,即任意两节点间的最长的路径长度
int left_len = 0; //左子树的最长深度
int right_len = 0; //右子树的最长深度
find(root, left_len, right_len, diameter);
return diameter;
}
void find(TreeNode *root,int &left_len,int &right_len,int &diameter){
int left = 0;
int right = 0;
if(root->left){
find(root->left,left,right,diameter);
left_len = left>right? left+1:right+1; //左子树最大深度取最大值 + 1
}
else{
left_len = 0;
}
if(root->right){
find(root->right,left,right,diameter);
right_len = left>right?left+1:right+1; //右子树最大深度取最大值 + 1
}
else{
right_len = 0;
}
diameter = diameter > left_len + right_len ? diameter : left_len + right_len;
//最大路径长度为左子树最大深度和右子树最大深度的和与原值取较大值
2. K 个一组翻转链表(LeetCode-25)
??:https://leetcode-cn.com/problems/reverse-nodes-in-k-group/
题目描述:
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
解题思路:
我们需要把链表节点按照 k 个一组分组,所以可以使用一个指针 head 依次指向每组的头节点。这个指针每次向前移动 k 步,直至链表结尾。对于每个分组,我们先判断它的长度是否大于等于 k。若是,我们就翻转这部分链表,否则不需要翻转。
class Solution {
public:
// 翻转一个子链表,并且返回新的头与尾
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
ListNode* prev = tail->next;
ListNode* p = head;
while (prev != tail) {
ListNode* nex = p->next;
p->next = prev;
prev = p;
p = nex;
}
return {tail, head};
}
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* hair = new ListNode(0);
hair->next = head;
ListNode* pre = hair;
while (head) {
ListNode* tail = pre;
// 查看剩余部分长度是否大于等于 k
for (int i = 0; i < k; ++i) {
tail = tail->next;
if (!tail) {
return hair->next;
}
}
ListNode* nex = tail->next;
// 这里是 C++17 的写法,也可以写成
// pair<ListNode*, ListNode*> result = myReverse(head, tail);
// head = result.first;
// tail = result.second;
tie(head, tail) = myReverse(head, tail);
// 把子链表重新接回原链表
pre->next = head;
tail->next = nex;
pre = tail;
head = tail->next;
}
return hair->next;
}
};
3. 最小覆盖子串(LeetCode-76)
??:https://leetcode-cn.com/problems/minimum-window-substring/
题目描述:
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
解题思路:
本问题要求我们返回字符串 ss 中包含字符串 tt 的全部字符的最小窗口。我们称包含 tt 的全部字母的窗口为「可行」窗口。
我们可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的 rr 指针,和一个用于「收缩」窗口的 ll 指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 ss 上滑动窗口,通过移动 rr 指针不断扩张窗口。当窗口包含 tt 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。
class Solution {
public:
unordered_map <char, int> ori, cnt;
bool check() {
for (const auto &p: ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
for (const auto &c: t) {
++ori[c];
}
int l = 0, r = -1;
int len = INT_MAX, ansL = -1, ansR = -1;
while (r < int(s.size())) {
if (ori.find(s[++r]) != ori.end()) {
++cnt[s[r]];
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
}
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]];
}
++l;
}
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};