简介:阿里巴巴校园招聘阿里云笔试试题文档,面向准备互联网大厂技术笔试的应届生与初中级开发者,聚焦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到64KB:
Path 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 |
| 临时文件 + BufferedStream | 8KB~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, "user@example.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()只找子串,比如user@example.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 Map<String, Integer> cache = new HashMap<>(); public Map<String, Integer> getCacheSnapshot() { return Collections.unmodifiableMap(new HashMap<>(cache)); }Collections.unmodifiableMap包了一层只读视图,外部调用put会抛UnsupportedOperationException;new 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 -ef | grep java` | 全命令行匹配 |
| `ps -ef | grep java | grep -v grep` |
jps -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('username=alibaba&role=intern');代码的执行顺序是:open建立连接但不发送,send才真正发出请求。onreadystatechange在每次readyState变化时触发,通常只关心readyState === 4(请求完成),此时responseText才是完整的服务器返回体。abort()用来取消当前请求,一般配合超时控制使用,不能在send后面同步调用,否则请求还没发出去就被取消。readyState的取值从0到4分别表示UNSENT、OPENED、HEADERS_RECEIVED、LOADING、DONE,这是面试里常考的状态表:
| readyState | 含义 | 可读响应数据 |
|---|---|---|
| 0 | 已创建XMLHttpRequest | 无 |
| 1 | 已调用open(),连接建立 | 无 |
| 2 | 已收到响应头 | responseHeaders |
| 3 | 正在接收响应体 | responseText一部分 |
| 4 | 响应体接收完成 | 完整responseText |
现在新项目更多用fetch,但fetch没有readyState,取消请求改用AbortController。笔试考老API并不是过时,而是借它考察异步机制的理解:为什么要在状态为4时才读取responseText,以及onreadystatechange和onload的差异。把状态表默写出来,这道题基本不会丢分。
4. 异或找单数、erase删中间元素:把空间和索引边界一起算给面试官看
4.1 数列里只有一个数出现一次,用异或比哈希表“便宜”在哪
题目:数列L有n=2k+1个整数,其中k个数字出现两次,1个数字出现一次。要求O(1)空间,尽快找出那个数。经典解法是异或:
int findUnique(const std::vector<int>& nums) { int answer = 0; for (int x : nums) { answer ^= x; } return answer; }answer初始为0,0与任何数异或仍等于该数;相同两个数异或的结果是0。因为异或满足交换律和结合律,所有出现两次的数字两两抵消,最后剩下的就是只出现一次的数。时间复杂度O(n),空间复杂度O(1),只用一个整型变量,比用哈希表统计次数省下大量内存。需要注意这题的前提是“其他数字恰好出现两次”,如果改成“一个数字出现一次,其他数字出现三次”,异或就不适用,要用位累加再对3取模。答题时可以主动说一句“这题能异或是利用了成对抵消的性质”,面试官通常就知道你理解了,而不是背答案。
4.2 删除vector第5、6、7号元素,从后往前erase和单次搬移
题目:有一个size=1000的vector<int>,删除其中的第5、6、7号元素,要求效率高。下标按从0开始记,就是删除下标4、5、6。最简单的写法是三次erase:
std::vector<int> 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三个要删的元素,读到readPos=7时写到下标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, (seg+1)*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.1,p = 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.4377,12块盘期望故障数λ = 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.9,nsf_annual ** 0.25对应“连续四个季度都不故障”的等价转换,(1 - q_quarterly) ** 12表示12块盘全部安全,用1减就是题目要求的“至少一块故障”。运行结果中q_quarterly约0.4377,最终概率约0.9990。笔试场景如果时间紧,可以直接写“约等于1”,但推导里要保留中间参数,因为阅卷更看重你能不能从4块盘的99.99%正确推出单盘季度故障率。
| 时间尺度 | 单盘不故障概率 | 12块盘至少一块故障概率 |
|---|---|---|
| 年 | 0.1 | 1 - 0.1^12 ≈ 1 |
| 季度 | 0.5623 | 1 - 0.5623^12 ≈ 0.999 |
6. 高可用自增ID:从文件+N的旧方案到可压测的工程实现
题目要求生成递增整型数字,高可用,宕机重启后仍递增。原题给了一个很实在的思路:文件里记录最大使用到的数字N,内存里记录当前使用最大数字,例如10;当内存使用到N-20时,往文件里写入N+50;宕机重启后从文件读到N,再预写N+50,然后继续计数。这个方案不依赖数据库,和系统时钟无关,恢复也快,但代价是会跳号。把这段逻辑写成可运行的骨架:
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,从nextSeed+1开始取号,所以最终结果单调递增,但中间可能空出几十个没用到的号。
验证这个方案是否真的高可用,我的做法是在一台云服务器上把它做成一个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,后者可以一次取一段区间再本地分配,思路和文件预写N+50完全一致。
| 实现方式 | 持久化介质 | 是否跳号 | 多实例支持 |
|---|---|---|---|
| 文件预写N+50 | 本地文件 | 是 | 否 |
| 数据库号段 | 数据库表 | 是 | 是 |
| Redis INCRBY | Redis | 否 | 是,但需考虑持久化 |
工程实现的差别只在于把“文件”换成“数据库/Redis”,把“N+50”换成“号段步长”,核心都是提前持久化水位,重启后从水位继续。能用这个压测命令验证出重复数为0,就说明这套“预写水位”在进程重启后确实保住了唯一性;至于跳号,那是允许付出的代价。
本文还有配套的精品资源,点击获取