大家好,我是小林。
前几天发了个上海的小厂面试,还有同学反馈想继续看小厂相关的面经。
那么,今天来分享北京某小厂的Java 后端面经 ,虽然没有算法,但是技术面问的问题还蛮多的,快接近 30 个面试题了,范围涉及网络+操作系统+mysql+java,还是蛮有压力。
考察的知识范围,我帮大家罗列一下:
网络:键入网址过程、DNS、HTTP、Cookie、TCP、网络攻击 操作系统:进程&线程、io 多路复用、epoll 和 select 区别 mysql:事务四大特性、隔离级别 Java:MVC分层、垃圾回收、面向对象、依赖注入、控制反转、依赖倒置 网络 输入URL的过程是怎么样的? 输入URL过程如下:
DNS 解析:当用户输入一个网址并按下回车键的时候,浏览器获得一个域名,而在实际通信过程中,我们需要的是一个 IP 地址,因此我们需要先把域名转换成相应 IP 地址。 TCP 连接:浏览器通过 DNS 获取到 Web 服务器真正的 IP 地址后,便向 Web 服务器发起 TCP 连接请求,通过 TCP 三次握手建立好连接。 建立TCP协议时,需要发送数据,发送数据在网络层使用IP协议, 通过IP协议将IP地址封装为IP数据报;然后此时会用到ARP协议,主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址,找到目的MAC地址; IP数据包在路由器之间,路由选择使用OPSF协议, 采用Dijkstra算法来计算最短路径树,抵达服务端。 发送 HTTP 请求:建立 TCP 连接之后,浏览器向 Web 服务器发起一个 HTTP 请求(如果是HTTPS协议,发送HTTP 请求之前还需要完成TLS四次握手); 处理请求并返回:服务器获取到客户端的 HTTP 请求后,会根据 HTTP 请求中的内容来决定如何获取相应的文件,并将文件发送给浏览器。 浏览器渲染:浏览器根据响应开始显示页面,首先解析 HTML 文件构建 DOM 树,然后解析 CSS 文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。 DNS的端口是多少? 默认端口号是53。
DNS的底层使用TCP还是UDP? DNS 基于UDP协议实现,DNS使用UDP协议进行域名解析和数据传输。因为基于UDP实现DNS能够提供低延迟、简单快速、轻量级的特性,更适合DNS这种需要快速响应的域名解析服务。
低延迟: UDP是一种无连接的协议,不需要在数据传输前建立连接,因此可以减少传输时延,适合DNS这种需要快速响应的应用场景。简单快速: UDP相比于TCP更简单,没有TCP的连接管理和流量控制机制,传输效率更高,适合DNS这种需要快速传输数据的场景。轻量级 :UDP头部较小,占用较少的网络资源,对于小型请求和响应来说更加轻量级,适合DNS这种频繁且短小的数据交换。尽管 UDP 存在丢包和数据包损坏的风险,但在 DNS 的设计中,这些风险是可以被容忍的。DNS 使用了一些机制来提高可靠性,例如查询超时重传、请求重试、缓存等,以确保数据传输的可靠性和正确性。
HTTP到底是不是无状态的? HTTP是无状态的,这意味着每个请求都是独立的,服务器不会在多个请求之间保留关于客户端状态的信息。在每个HTTP请求中,服务器不会记住之前的请求或会话状态,因此每个请求都是相互独立的。
虽然HTTP本身是无状态的,但可以通过一些机制来实现状态保持,其中最常见的方式是使用Cookie和Session来跟踪用户状态。通过在客户端存储会话信息或状态信息,服务器可以识别和跟踪特定用户的状态,以提供一定程度的状态保持功能。
携带Cookie的HTTP请求是有状态还是无状态的?Cookie是HTTP协议簇的一部分,那为什么还说HTTP是无状态的? 携带Cookie的HTTP请求实际上是可以在一定程度上实现状态保持的,因为Cookie是用来在客户端存储会话信息和状态信息的一种机制。当浏览器发送包含Cookie的HTTP请求时,服务器可以通过读取这些Cookie来识别用户、管理会话状态以及保持特定的用户状态。因此,可以说即使HTTP本身是无状态的协议,但通过Cookie的使用可以实现一定程度的状态保持功能。
HTTP被描述为“无状态”的主要原因是每个HTTP请求都是独立的,服务器并不保存关于客户端的状态信息,每个请求都需要提供足够的信息来理解请求的意图。这样的设计使得Web系统更具有规模化和简单性,但也导致了一些挑战,比如需要额外的机制来处理用户状态和会话管理。
虽然Cookie是HTTP协议簇的一部分,但是HTTP协议在设计初衷上仍然保持无状态特性,即每个请求都是相互独立的。使用Cookie只是在无状态协议下的一种补充机制,用于在客户端存储状态信息以实现状态保持。
Cookie和Session的区别 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。 存取方式的不同,Cookie只能保存 ASCII,Session可以存任意数据类型,比如UserId等。 有效期不同,Cookie可设置为长时间保持,比如默认登录功能功能,Session一般有效时间较短,客户端关闭或者Session超时都会失效。 隐私策略不同,Cookie存储在客户端,信息容易被窃取;Session存储在服务端,相对安全一些。 存储大小不同, 单个Cookie 保存的数据不能超过 4K,Session可存储数据远高于Cookie。 如果我把数据存储到 localStorage,和Cookie有什么区别? 存储容量: Cookie 的存储容量通常较小,每个 Cookie 的大小限制在几 KB 左右。而 LocalStorage 的存储容量通常较大,一般限制在几 MB 左右。因此,如果需要存储大量数据,LocalStorage 通常更适合; 数据发送: Cookie 在每次 HTTP 请求中都会自动发送到服务器,这使得 Cookie 适合用于在客户端和服务器之间传递数据。而 localStorage 的数据不会自动发送到服务器,它仅在浏览器端存储数据,因此 LocalStorage 适合用于在同一域名下的不同页面之间共享数据; 生命周期:Cookie 可以设置一个过期时间,使得数据在指定时间后自动过期。而 LocalStorage 的数据将永久存储在浏览器中,除非通过 JavaScript 代码手动删除; 安全性:Cookie 的安全性较低,因为 Cookie 在每次 HTTP 请求中都会自动发送到服务器,存在被窃取或篡改的风险。而 LocalStorage 的数据仅在浏览器端存储,不会自动发送到服务器,相对而言更安全一些; 什么数据应该存在到cookie,什么数据存放到 Localstorage Cookie 适合用于在客户端和服务器之间传递数据、跨域访问和设置过期时间,而 LocalStorage 适合用于在同一域名下的不同页面之间共享数据、存储大量数据和永久存储数据
tcp为什么需要三次握手建立连接? 三次握手的原因:
三次握手才可以阻止重复历史连接的初始化(主要原因) 三次握手才可以同步双方的初始序列号 三次握手才可以避免资源浪费 原因一:避免历史连接
我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因 :
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
我们考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)报文(_注意!不是重传 SYN,重传的 SYN 的序列号是一样的_)。
看看三次握手是如何阻止历史连接的:
三次握手避免历史连接
客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵 情况下:
一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。 客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。 服务端收到 RST 报文后,就会释放连接。 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。 上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接 。
如果是两次握手连接,就无法阻止历史连接 ,那为什么 TCP 两次握手为什么无法阻止历史连接呢?
我先直接说结论,主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费 。
你想想,在两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST 报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会断开连接。
两次握手无法阻止历史连接
可以看到,如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手 。
所以,TCP 使用三次握手建立连接的最主要原因是防止「历史连接」初始化了连接。
原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
接收方可以去除重复的数据; 接收方可以根据数据包的序列号按序接收; 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道); 可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手与三次握手
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步 ,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接 ,这会造成什么情况呢?
如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
两次握手会造成资源浪费
即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN 报文,而造成重复分配资源。
SQL注入问题是什么? SQL注入发生在当应用程序直接使用用户提供的输入作为SQL查询的一部分时。当用户输入被错误地用作数据库查询的一部分,而应用程序没有对其进行适当的验证和转义,就可能会发生SQL注入。
例如,如果一个用户输入了一个字符串来查找特定用户的信息,但应用程序将此用户输入直接用作SQL查询的一部分(例如,作为SELECT语句的一部分),而不考虑可能的安全问题,那么攻击者可能会利用这一点来执行他们自己的恶意SQL查询。
解决SQL注入问题的方法主要有以下几种:
输入验证和转义:在将用户输入用作SQL查询的一部分之前,对输入进行验证和转义。确保输入符合预期格式,并防止任何可能导致SQL注入的特殊字符。 使用参数化查询:使用参数化查询可以避免直接将用户输入嵌入到SQL查询中。参数化查询使用预定义的变量来接收用户输入,并将其传递给数据库引擎,而不是直接将其用作查询的一部分。这样可以防止SQL注入攻击。 限制数据库权限:限制数据库用户的权限,只授予他们执行所需操作所需的最低权限。攻击者可能具有比预期更多的权限,这可能会使攻击更加容易。 实施输入过滤:在某些情况下,实施输入过滤可以进一步减少SQL注入的风险。这可能涉及检查和过滤用户输入中的特殊字符和词汇,以排除可能的恶意输入。 CSRF攻击是什么? CSRF(跨站请求伪造)是一种攻击手段,攻击者通过诱导用户执行恶意操作,从而获取用户数据或执行恶意代码。CSRF攻击通常通过伪造一个合法的HTTP请求来实现,这个请求看起来是合法的,但实际上是为了执行一个攻击者控制的操作。
解决CSRF攻击的方法主要有以下几种:
验证用户会话:在服务器端对用户会话进行验证,确保请求的会话标识符与当前会话标识符匹配。这样可以防止攻击者伪造会话标识符。 使用双重验证:除了会话验证,还可以使用其他验证方式,例如验证码、签名验证等。这些验证方式可以增加攻击的难度。 防止跨站请求:通过设置CSP(内容安全策略)来防止跨站请求,限制网页中可执行的脚本源,减少攻击者诱导用户执行恶意操作的可能性。 避免使用自动提交表单:禁用默认的自动提交功能,要求用户在提交表单前确认操作,防止攻击者诱导用户在未经授权的情况下提交表单。 强制Referer头部:在服务器端检查请求的Referer头部,确保请求来自可信来源。 XSS攻击是什么? XSS是跨站脚本攻击,攻击者通过在Web页面中插入恶意脚本代码,然后诱使用户访问该页面,从而使得恶意脚本在用户浏览器中执行,从而盗取用户信息、会话信息等敏感数据,甚至控制用户账户。
XSS 攻击可以分为 3 类:存储型(持久型)、反射型(非持久型)、DOM 型。
存储型 XSS:注入型脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器上传回并执行。 反射型 XSS:当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web 服务器将注入脚本,比如一个错误信息,搜索结果等 返回到用户的浏览器上。由于浏览器认为这个响应来自"可信任"的服务器,所以会执行这段脚本。 基于 DOM 的 XSS:通过修改原始的客户端代码,受害者浏览器的 DOM 环境改变,导致有效载荷的执行。也就是说,页面本身并没有变化,但由于 DOM 环境被恶意修改,有客户端代码被包含进了页面,并且意外执行。 预防XSS攻击的方法主要包括以下几点:
输入验证:对所有用户输入的数据进行有效性检验,过滤或转义特殊字符。例如,禁止用户输入HTML标签和JavaScript代码。 输出编码:在网页输出用户输入内容时,使用合适的编码方式,如HTML转义、URL编码等,防止恶意脚本注入。 Content Security Policy(CSP):通过设置CSP策略,限制网页中可执行的脚本源,有效防范XSS攻击。 使用HttpOnly标记:在设置Cookie时,设置HttpOnly属性,使得Cookie无法被JavaScript代码读取,减少受到XSS攻击的可能。 服务端HTTP响应的端口是多少? 默认是 80
操作系统 进程和线程的区别? 本质区别 :进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位在开销方面 :每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小稳定性方面 :进程中某个线程如果崩溃了,可能会导致整个进程都崩溃。而进程中的子进程崩溃,并不会影响其他进程。内存分配方面 :系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源包含关系 :没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程epoll 和 select 的区别? select 和 poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的 Socket 集合。
在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。
很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。
epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题。
epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。 epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。 io多路复用的用途是什么? io 多路复用可以只需要1 个线程就能并发处理多个客户端 i/o 事件,减少了需要创建大量线程来处理多个I/O事件的系统开销,比如线程上下文切换,占用cpu资源。
MySQL 数据库ACID是指什么? 原子性(Atomicity) :一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。一致性(Consistency) :是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。隔离性(Isolation) :数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。持久性(Durability) :事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。默认隔离级别是什么 可重复读隔离级别
可重复读隔离级别下,A事务提交的数据,在B事务能看见吗? 可重复读隔离级是由 MVCC(多版本并发控制)实现的,实现的方式是开始事务后(执行 begin 语句后),在执行第一个查询语句后,会创建一个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的 ,即使中途有其他事务插入了新纪录,是查询不出来这条数据的。
Java MVC分层介绍一下 MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。
视图(view):为用户提供使用界面,与用户直接进行交互。 模型(model):代表一个存取数据的对象或 JAVA POJO(Plain Old Java Object,简单java对象)。它也可以带有逻辑,主要用于承载数据,并对用户提交请求进行计算的模块。模型分为两类,一类称为数据承载 Bean,一类称为业务处理Bean。所谓数据承载 Bean 是指实体类(如:User类),专门为用户承载业务数据的;而业务处理 Bean 则是指Service 或 Dao 对象, 专门用于处理用户提交请求的。 控制器(controller):用于将用户请求转发给相应的 Model 进行处理,并根据 Model 的计算结果向用户提供相应响应。它使视图与模型分离。 流程步骤:
用户通过View 页面向服务端提出请求,可以是表单请求、超链接请求、AJAX 请求等; 服务端 Controller 控制器接收到请求后对请求进行解析,找到相应的Model,对用户请求进行处理Model 处理; 将处理结果再交给 Controller(控制器其实只是起到了承上启下的作用); 根据处理结果找到要作为向客户端发回的响应View 页面,页面经渲染后发送给客户端。 Java的内存回收机制是怎么样的? Java的内存回收机制基于自动内存管理,开发人员无需手动释放内存。垃圾回收器会自动识别不再使用的对象,并回收它们所占用的内存空间。
垃圾回收算法主要有 :
标记-清除算法 :标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。复制算法 :为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。标记-整理算法 :复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。分代回收算法 :分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。垃圾回收算法是什么,是为了解决了什么问题? JVM有垃圾回收机制的原因是为了解决内存管理的问题。在传统的编程语言中,开发人员需要手动分配和释放内存,这可能导致内存泄漏、内存溢出等问题。而Java作为一种高级语言,旨在提供更简单、更安全的编程环境,因此引入了垃圾回收机制来自动管理内存。
垃圾回收机制的主要目标是自动检测和回收 不再使用的对象,从而释放它们所占用的内存空间。这样可以避免内存泄漏(一些对象被分配了内存却无法被释放,导致内存资源的浪费)。同时,垃圾回收机制还可以防止内存溢出(即程序需要的内存超过了可用内存的情况)。
通过垃圾回收机制,JVM可以在程序运行时自动识别和清理不再使用的对象,使得开发人员无需手动管理内存。这样可以提高开发效率、减少错误,并且使程序更加可靠和稳定。
怎么判断一个对象是否可以被回收? 垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。一般有两种方法来判断:
引用计数器法 :为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;可达性分析算法 :从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
面向对象的多态是指什么?多态性是指同一个方法调用可以在不同对象上产生不同的行为。多态性是面向对象编程的一个重要特性,使得程序可以根据对象的实际类型来调用相应的方法,而不是根据引用变量的类型。
具体来说,多态性可以通过继承和方法重写实现。当子类重写父类的方法时,可以根据实际对象的类型来确定调用哪个方法实现,从而实现多态性。
通过多态性,可以实现代码的灵活性和可扩展性,提高代码的复用性和可维护性。
依赖倒置,依赖注入,控制反转分别是什么? 控制反转:“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。 依赖注入:依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。 依赖倒置:这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。 怎么用依赖倒置的思想对代码进行设计 依赖倒置原则通俗说就是,高层模块不依赖低层模块,而是都依赖抽象接口,这个抽象接口通常是由高层模块定义,低层模块实现。
遵循依赖倒置原则有这样几个编码守则:
应用代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。 不要继承具体类,如果一个类在设计之初不是抽象类,那么尽量不要去继承它。对具体类的继承是一种强依赖关系,维护的时候难以改变。 不要重写(override)包含具体实现的函数。 依赖倒置原则最典型的使用场景就是框架的设计。框架提供框架核心功能,比如 HTTP 处理,MVC 等,并提供一组接口规范,应用程序只需要遵循接口规范编程,就可以被框架调用。程序使用框架的功能,但是不调用框架的代码,而是实现框架的接口,被框架调用,从而框架有更高的可复用性,被应用于各种软件开发中。
怎么实现依赖注入,如果没有容器 即在没有使用任何依赖注入容器的情况下管理对象之间的依赖关系。一个常见的方法是通过构造函数或setter方法注入依赖项。以下是一个简单的示例,展示了如何在没有容器的情况下手动进行依赖注入:
假设我们有一个Service类和一个Repository类的示例,Service类依赖于Repository类。我们将手动创建这些对象并将Repository实例传递给Service类的构造函数。
// Repository 类
public class Repository {
public String getData() {
return "Data from repository";
}
}
// Service 类
public class Service {
private Repository repository;
public Service(Repository repository) {
this.repository = repository;
}
public void doSomething() {
String data = this.repository.getData();
System.out.println("Doing something with data: " + data);
}
}
// 手动创建对象并进行依赖注入
public class Main {
public static void main(String[] args) {
Repository repository = new Repository();
Service service = new Service(repository);
// 使用Service对象
service.doSomething();
}
}
在这个示例中,Service类的构造函数接受一个Repository实例作为参数,并将其存储在类的实例变量中。这种方式通过构造函数手动注入依赖项,使得对象之间的依赖关系更清晰,并且更容易进行单元测试。
虽然这种手动依赖注入的方式适用于简单的情况,但在实际项目中,随着项目的复杂性增加,可能需要更灵活和自动化的依赖注入机制,这时候依赖注入容器就会发挥更大的作用。常见的依赖注入容器有Spring Framework中的Spring IoC容器等,它们可以更好地管理和解决依赖关系。