ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

阿里云笔试题全解析:Java、Linux、位图与高可用ID设计

阿里云笔试题全解析:Java、Linux、位图与高可用ID设计 简介阿里巴巴校园招聘阿里云笔试试题文档面向准备互联网大厂技术笔试的应届生与初中级开发者聚焦Java编程、Linux命令、Ajax、算法与数据结构、概率论及系统设计等高频考点。资源共1个doc文件压缩包大小仅17KB内容紧凑涵盖文件复制读写、正则表达式、HashMap封装影响、线程安全单例、位图算法、40亿整数去重、高可用递增ID生成等经典题目与解析可作为笔试冲刺阶段的浓缩复习提纲。目前已有369人学习浏览适合在短时间内快速梳理阿里云笔试核心题型与解题思路。通过学习这份资料读者能掌握从HashMap设计到Linux进程过滤、从Ajax请求流程到概率计算与系统设计的具体应对方法并结合代码示例理解底层原理与工程实践要点便于查漏补缺、举一反三。1. 这份阿里云笔试题为什么说它不是背题能过的拿到这套阿里云校园招聘笔试题目的人第一反应通常是“Java文件复制、正则、HashMap都会”等做到第7题概率题和第10题40亿整数找缺失就卡住了最后第11题“宕机重启后仍递增”更像一个系统设计题。它把基础API、Linux命令、前端交互、C容器、位图和概率论揉在一张卷子里既考“会不会”也考“在受限资源下怎么取舍”。下面把11道题按“Java IO → Linux/Ajax → 算法 → 大数据位图 → 概率 → 高可用ID”的顺序拆开每一题都给出可运行的代码、命令或推导并标注边界参数。适合正在准备大厂校招以及想补Java后端和大数据基础的工程师。2. 文件复制、正则与HashMap三条Java题背后的边界意识2.1 把文件内容复制两遍先想清楚“读什么、写哪里”题目要求有一个文件c:/c.txt写Java程序把该文件内容复制两遍追加到c:/c.txt。最直接的写法是先把整个文件读进内存然后以追加模式写两次import java.nio.file.*; import java.io.IOException; public class DuplicateFile { public static void main(String[] args) throws IOException { Path file Paths.get(C:/c.txt); byte[] data Files.readAllBytes(file); // 第二参数 true 表示追加写不会覆盖原文件 try (java.io.FileOutputStream out new java.io.FileOutputStream(file.toFile(), true)) { out.write(data); out.write(data); } } }这个方案简单直观适合几十MB以内的文件。注意FileOutputStream(file.toFile(), true)里的true是append开关缺省会截断文件Files.readAllBytes会把整个文件加载到堆内存如果线上服务器有一个4GB的日志文件这一步就会OOM。常见做法是先把原文件复制到临时文件再分两次把临时文件内容追加回原文件缓冲区通常设8KB到64KBPath original Paths.get(C:/c.txt); Path snapshot Files.createTempFile(c_snapshot, .txt); Files.copy(original, snapshot, StandardCopyOption.REPLACE_EXISTING); byte[] buffer new byte[8192]; for (int pass 0; pass 2; pass) { try (var in new java.io.BufferedInputStream(Files.newInputStream(snapshot)); var out new java.io.BufferedOutputStream(Files.newOutputStream( original, StandardOpenOption.CREATE, StandardOpenOption.APPEND))) { int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } } }后面这段代码先把原文件内容“快照”到临时文件避免一边读一边追加造成死循环式地把新数据读进来外层循环执行两次每次读取临时文件并追加到原文件末尾。Files.newOutputStream默认会覆盖文件所以必须显式加CREATE和APPEND两个StandardOpenOption。buffer的大小会影响IO次数8KB是通用选择日志文件在机械盘上可以调到64KB减少系统调用。笔试如果只要求写出思路说清“先读原内容再追加两份注意避免读到追加内容”就能得分。方案内存占用适用文件大小风险readAllBytes 两次write整个文件几十MB以内大文件OOM临时文件 BufferedStream8KB~64KB任意大小需要额外临时空间2.2 邮箱和数字的正则matches()和find()是两回事题目要求写正则表达式一个校验邮箱一个校验数字。主干代码import java.util.regex.Pattern; public class RegexDemo { public static void main(String[] args) { String email ^[A-Za-z0-9._%-][A-Za-z0-9.-]\\.[A-Za-z]{2,}$; String number ^[0-9]$; System.out.println(Pattern.matches(email, userexample.com)); // true System.out.println(Pattern.matches(number, 12345)); // true } }邮箱正则的[A-Za-z0-9._%-]匹配用户名部分后面是域名最后\\.[A-Za-z]{2,}要求有至少两个英文字母的顶级域。数字正则用[0-9]只能匹配纯非负整数如果要匹配负数或小数需要改成^-?\\d(\\.\\d)?$。Pattern.matches内部使用Matcher.matches()要求整个字符串完全匹配而find()只找子串比如userexample.com.cn中包含example也能被find()命中容易误判。笔试里常挖这个坑所以答题时最好写清“用matches()做全量校验”而不是find()。如果需要批量校验把Pattern对象复用而不是每次都Pattern.compile能省掉重复编译的正则表达式开销。2.3 HashMap改变map类用户的代码为什么不用动题干是“HashMap改变map类对用户会不会有影响”。这是个偏设计的问题用户依赖的是Map接口不依赖HashMap的特定实现。JDK 8以后HashMap在链表长度超过阈值时转红黑树可读写API、与用户交互的行为没有变调用方只要面向Map编程就无感。但如果用户代码悄悄依赖了HashMap的迭代顺序或者直接拿到内部Entry做修改就可能受影响。常见的防御做法是把内部集合以不可变视图暴露出去private final MapString, Integer cache new HashMap(); public MapString, Integer getCacheSnapshot() { return Collections.unmodifiableMap(new HashMap(cache)); }Collections.unmodifiableMap包了一层只读视图外部调用put会抛UnsupportedOperationExceptionnew HashMap(cache)做一次浅拷贝避免外部拿到引用后原地修改内部状态。这样的封装让“HashMap未来怎么改”都不影响调用方也符合常见的Java编码规范里“集合不直接暴露给外部”的要求。答题时先讲“接口与实现分离”再补一句“内部结构变化不影响外部API但迭代顺序和线程安全性仍要由调用方确认”就能比只写结论多拿一点分。提示笔试题如果只写Files.readAllBytes大文件场景会被追问OOM最好带一句“小文件用readAllBytes大文件用临时文件流式写”。3. ps -ef | grep java 与 Ajax 五态运维和前端交叉点上的基础题3.1 查看Java进程一个能直接用的命令和三个生产环境补充题目问“Linux 中需查看所有的 java 进程用什么命令”标准答法是ps -ef | grep java这个命令把ps输出的所有进程信息里包含java的行过滤出来。但直接这样写有一个经典问题grep java这个进程本身也包含“java”四个字母会被过滤出来。如果在一个没有Java进程的服务器上执行依然能看到两行输出其中一行是grep java。所以生产环境里更严谨的写法是ps -ef | grep java | grep -v grepgrep -v grep是排除包含grep的行。如果安装了JDK还可以直接用jps -l它只列出Java进程的PID和主类/启动类不依赖grep。下面是三类命令的对比命令输出内容适用场景ps -efgrep java全命令行匹配ps -efgrep javagrep -v grepjps -l只列Java进程本机有JDK、同用户运行在阿里云ECS上排查Java应用时我一般还会配合top -p pid看CPU再jstack pid看线程栈。笔试题只要求命令答出ps -ef|grep java就能过面试延伸问到“进程为什么起不来”就要能说清先看日志、再看jps是否列出了进程、最后用jstack定位阻塞。3.2 Ajax全流程open到send之间少一个状态处理器题目列了open()、send()、abort()、readyState、responseText要求讲整个Ajax流程。一个完整的原生XMLHttpRequest请求长这样const xhr new XMLHttpRequest(); xhr.open(POST, /api/login, true); xhr.onreadystatechange function () { if (xhr.readyState 4) { clearTimeout(timer); if (xhr.status 200) { console.log(xhr.responseText); } else { console.error(HTTP error: xhr.status); } } }; // 超过 3 秒没有完成请求就取消避免页面一直转圈 const timer setTimeout(() xhr.abort(), 3000); xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); xhr.send(usernamealibabaroleintern);代码的执行顺序是open建立连接但不发送send才真正发出请求。onreadystatechange在每次readyState变化时触发通常只关心readyState 4请求完成此时responseText才是完整的服务器返回体。abort()用来取消当前请求一般配合超时控制使用不能在send后面同步调用否则请求还没发出去就被取消。readyState的取值从0到4分别表示UNSENT、OPENED、HEADERS_RECEIVED、LOADING、DONE这是面试里常考的状态表readyState含义可读响应数据0已创建XMLHttpRequest无1已调用open()连接建立无2已收到响应头responseHeaders3正在接收响应体responseText一部分4响应体接收完成完整responseText现在新项目更多用fetch但fetch没有readyState取消请求改用AbortController。笔试考老API并不是过时而是借它考察异步机制的理解为什么要在状态为4时才读取responseText以及onreadystatechange和onload的差异。把状态表默写出来这道题基本不会丢分。4. 异或找单数、erase删中间元素把空间和索引边界一起算给面试官看4.1 数列里只有一个数出现一次用异或比哈希表“便宜”在哪题目数列L有n2k1个整数其中k个数字出现两次1个数字出现一次。要求O(1)空间尽快找出那个数。经典解法是异或int findUnique(const std::vectorint nums) { int answer 0; for (int x : nums) { answer ^ x; } return answer; }answer初始为00与任何数异或仍等于该数相同两个数异或的结果是0。因为异或满足交换律和结合律所有出现两次的数字两两抵消最后剩下的就是只出现一次的数。时间复杂度O(n)空间复杂度O(1)只用一个整型变量比用哈希表统计次数省下大量内存。需要注意这题的前提是“其他数字恰好出现两次”如果改成“一个数字出现一次其他数字出现三次”异或就不适用要用位累加再对3取模。答题时可以主动说一句“这题能异或是利用了成对抵消的性质”面试官通常就知道你理解了而不是背答案。4.2 删除vector第5、6、7号元素从后往前erase和单次搬移题目有一个size1000的vectorint删除其中的第5、6、7号元素要求效率高。下标按从0开始记就是删除下标4、5、6。最简单的写法是三次erasestd::vectorint v(size1000); v.erase(v.begin() 6); v.erase(v.begin() 5); v.erase(v.begin() 4);这里必须从后往前删。如果先删begin()4原下标5的元素会前移到下标4再删begin()5就删错了对象。从后往前删每次删除的位置都在当前有效范围内索引不会偏移。但vector::erase每次删除都要把后面所有元素向前搬移三次erase会搬移三批数据。更高效的做法是一次遍历把不需要删除的元素整体前移int writePos 4; // 保留前 4 个元素 for (int readPos 7; readPos v.size(); readPos) { v[writePos] v[readPos]; } v.resize(writePos);第一次循环从原下标7开始读也就是跳过了5、6、7三个要删的元素读到readPos7时写到下标4之后连续前移。最后resize把尾部多余元素截掉。这种方式只搬移一次元素数据量越大优势越明显。要删除的位置如果是一串递增下标都可以用“双指针跳过区间”的思路而不是多次erase。题目特意强调size是1000就是想让答题者说明白1000个元素不算大但要求效率高说明要考搬移次数而不是只考API记忆。方法元素搬移次数索引处理从前往后三次erase多需要固定原始下标从后往前三次erase中等下标不偏移双指针跳过区间一次适合删除连续区间4.3 40亿个整数找缺失位图为什么能装进256M题目给了一个极值问题文件里有40亿个不重复整数取值范围是0~4294967295可用内存256M找出不在文件里的约2.9亿个数。先算一笔账32位整数总共2^32个用1个bit表示一个数是否出现过需要2^32/8 512MB。256M放不下所以要分段。把整个取值空间切成16段每段有2^28个整数对应位图大小是2^28/8 32MB放进256M内存绰绰有余。处理流程是for 段号 seg in 0..15: 创建 32MB 的 bitmap初始全 0 遍历文件中所有 40 亿个整数 n: 如果 n 落在 [seg*2^28, (seg1)*2^28) 内: 把 n 对应的 bit 置为 1 遍历 bitmap 输出所有仍为 0 的 bit 对应的整数按这个流程每个数会被扫描16遍看起来多但全部是顺序读落到SSD上可以接受。每次只保留32MB内存正好卡在256M限制内。原题描述里提到的“分段载入内存排序输出”也是同一个思路分段后每段数据量是2^28个完全可以排序成有序文件再取缺失值。位图法更省空间但要求数据不重复。如果数据允许重复位图的1个bit就表达不了“出现两次”的频率需要换成计数位图或者先做一次去重。答题时把512MB与32MB的换算过程写出来面试官能立刻看出你是否真的会算而不是背了个“位图”名词。5. 硬盘故障概率四块盘99.99%反推出单盘年故障率5.1 从“至少一块故障”反推单盘故障率题干一个包含4块硬盘的服务器一年中至少有一块硬盘出故障的概率是99.99%每块硬盘任意时刻出故障的概率服从相同的分布规律并且彼此独立。问12块硬盘的服务器一季度内至少有一个硬盘出故障的概率。这里的关键是先从4块盘反推单块盘的年故障率再换到季度时间尺度。设一块硬盘一年的故障概率是p。因为彼此独立“4块盘一年内都不出故障”的概率是(1-p)^4而“至少一块出故障”1-(1-p)^4 0.9999。所以(1-p)^4 0.0001开四次方得到1-p 0.1p 0.9。也就是说题目隐含的假设是单块盘年故障率高达90%。这个数字明显不符合现实但在“独立同分布”的假设下只能这样反推。5.2 季度换算不能直接除以4接下来看一季度。不能简单用0.9/4去算季度故障率因为“一年内至少故障一次”不等于“故障时刻在一季度均匀摊平”。合理做法是认为每个季度是否出故障的概率相同且独立那么“连续四个季度都不故障” (1 - 季度故障率)^4 1 - p 0.1。因此季度无故障率 0.1^(1/4) ≈ 0.5623季度故障率q ≈ 0.4377。12块盘在一季度内“至少一块故障” 1 - (1-q)^12 1 - 0.5623^12。0.5623^12约等于0.001所以最终概率约99.9%。如果不想用指数也可以用泊松近似把年故障率折算成季度后q≈0.437712块盘期望故障数λ 12×0.4377 ≈ 5.25至少一块故障1-e^{-5.25}≈99.5%和精确值接近。笔试里写出推导步骤就能拿分最后给结论还应当补一句这个概率接近1并不代表存储方案必坏因为真实云盘单块年故障率远低于90%在阿里云这类云环境中云盘靠多副本和故障迁移来保证数据可用性而不是赌单盘不坏。5.3 用 Python 把概率结果验算一遍这类概率题手算容易漏指数我习惯用一段小代码验算顺便把时间尺度参数留成变量p_annual 0.9 # 单盘年故障率 nsf_annual 1 - p_annual # 单盘全年不故障概率 0.1 q_quarterly 1 - nsf_annual ** 0.25 # 单盘季度故障率 p_12_one_quarter 1 - (1 - q_quarterly) ** 12 print(单盘季度故障率:, q_quarterly) print(12块盘一季度至少坏一块:, p_12_one_quarter)p_annual是反推出来的0.9nsf_annual ** 0.25对应“连续四个季度都不故障”的等价转换(1 - q_quarterly) ** 12表示12块盘全部安全用1减就是题目要求的“至少一块故障”。运行结果中q_quarterly约0.4377最终概率约0.9990。笔试场景如果时间紧可以直接写“约等于1”但推导里要保留中间参数因为阅卷更看重你能不能从4块盘的99.99%正确推出单盘季度故障率。时间尺度单盘不故障概率12块盘至少一块故障概率年0.11 - 0.1^12 ≈ 1季度0.56231 - 0.5623^12 ≈ 0.9996. 高可用自增ID从文件N的旧方案到可压测的工程实现题目要求生成递增整型数字高可用宕机重启后仍递增。原题给了一个很实在的思路文件里记录最大使用到的数字N内存里记录当前使用最大数字例如10当内存使用到N-20时往文件里写入N50宕机重启后从文件读到N再预写N50然后继续计数。这个方案不依赖数据库和系统时钟无关恢复也快但代价是会跳号。把这段逻辑写成可运行的骨架public class SafeIncrement { private int current; private int highWatermark; private int nextSeed; private static final int STEP 50; public synchronized int next() { if (current highWatermark) { nextSeed nextSeed STEP; writeFile(nextSeed); highWatermark nextSeed - 10; } return current; } }next()是synchronized方法保证单机多线程只取到不同值highWatermark控制何时落盘而不是每次取号都写文件降低IO频率。重启时读文件得到nextSeed从nextSeed1开始取号所以最终结果单调递增但中间可能空出几十个没用到的号。验证这个方案是否真的高可用我的做法是在一台云服务器上把它做成一个HTTP接口然后用循环请求加kill -9重启来测重复for i in $(seq 1 10000); do curl -s http://127.0.0.1:8080/next; echo; done \ | sort -n | uniq -d | wc -l管道里sort -n把取到的ID按数字排序uniq -d输出重复行wc -l统计重复行数。如果输出是0说明当前部署下没有生成相同ID如果重启前已经取到500重启后从文件里的nextSeed继续ID会出现空洞但不会重复。注意这套方案只适合单节点一旦部署成多实例每个实例的nextSeed可能相同依然会撞号。生产上更常见的是数据库号段模式或Redis的INCRBY后者可以一次取一段区间再本地分配思路和文件预写N50完全一致。实现方式持久化介质是否跳号多实例支持文件预写N50本地文件是否数据库号段数据库表是是Redis INCRBYRedis否是但需考虑持久化工程实现的差别只在于把“文件”换成“数据库/Redis”把“N50”换成“号段步长”核心都是提前持久化水位重启后从水位继续。能用这个压测命令验证出重复数为0就说明这套“预写水位”在进程重启后确实保住了唯一性至于跳号那是允许付出的代价。本文还有配套的精品资源点击获取
返回列表