
搜狐畅游2019校招的这套Java开发工程师笔试题我拿到手的时候第一感觉是这卷子的出题风格和市面上大多数Java面试八股文题库有很明显的区别。游戏公司的Java岗位笔试除了通用基础语法之外会格外重视你对集合底层、异常机制、以及手写排序算法这类“真功夫”的考察。如果你只会背概念、没动手写过代码做题的时候会明显觉得拧巴——很多题看起来都眼熟但就是拿不准答案。这篇文章不打算复述原题而是把这套笔试题背后真正想考察的Java核心能力拆开讲清楚。无论你是准备校招的应届生还是想检验自己Java基础是否扎实的开发者都可以对照这篇文章自查一遍。我尽量把每类题型的考察逻辑、标准答案、以及容易踩的坑都写明白方便你直接照着补短板。1. 搜狐畅游这类游戏公司笔试题的考察重心不光是背八股1.1 游戏公司Java岗笔试和互联网通用岗的差异很多同学准备的Java面试题都是从网上找的那种“高频题合集”背HashMap原理、背JVM内存模型、背Spring生命周期。这些东西有没有用有用但游戏公司的笔试有自己的侧重点。游戏后端Java岗位实际工作中打交道最多的是什么是高并发下的玩家数据读写、排行榜计算、道具发放、战斗结算这类逻辑。这些业务场景决定了笔试会偏向以下方向集合类的线程安全与性能、排序算法的掌握程度、异常处理的理解深度、以及Java语法细节的熟练度。相比之下Spring框架的题目反而占比不大——因为校招更看重底层基础框架可以入职后再学。搜狐畅游这套笔试题的题型分布我根据自己的经验和类似的校招真题做了一份拆解大致是这样的考察模块预估占比常见题型Java语法基础与面向对象35%-40%选择题、读代码写输出集合框架与常用类20%-25%选择题、简答题算法与排序手写15%-20%手写代码题JVM内存与异常机制10%-15%选择题、判断题Java新特性Lambda等5%-10%读代码题、简单应用1.2 90分钟的答题节奏怎么把握这类笔试题量一般在30到40道题之间加上一两道手写代码题。我的建议是选择题部分控制在45分钟内完成读代码题控制在20分钟最后留至少20分钟给手写题。手写题不一定很难但很考察你平时有没有真正写过——很多人知道快排的思路一让写代码就卡壳。另外要注意读代码题的坑在于“你以为你看懂了”但输出的结果和实际运行结果不一样。这类题基本都在考你Java的细节机制比如初始化顺序、重载匹配、自动装箱缓存、finally执行时机等等。这些细节不是靠背能背出来的得靠平时写代码时养成的敏感性。2. Java基础语法与面向对象看似送分却最容易丢分的细节题2.1 面向对象三大特性的隐藏考点封装、继承、多态这六个字谁都能说上来但笔试不会直接问“什么是封装”而是用代码题来考你对访问控制符的理解。我印象很深的一道经典题目在同一个包下定义了A和B两个类A中有一个protected修饰的字段B继承了A且B与A不在同一个包中问B能不能访问这个字段。很多人会答错。正确答案是在B的内部方法中可以直接访问这个protected字段但如果通过A的实例来访问则不行——protected允许子类在自身内部访问从父类继承来的protected成员但不允许通过父类引用去访问。继承还有一个必考点构造器加载顺序。笔试常给一段父类子类都有静态块、实例块、构造器的代码让你写出创建子类对象时的输出。标准答案是父类静态块 → 子类静态块 → 父类实例块 → 父类构造器 → 子类实例块 → 子类构造器。这个顺序建议直接刻在脑子里几乎每一套Java笔试题都会以某种形式出现。多态部分常见的坑是重载和重写的混淆。记住这句话重载看编译期类型重写看运行期类型。题目里如果有方法参数是父类型、实际传的是子类型那调用的就是父类型对应的方法但如果一个方法被子类重写了则调用的一定是子类版本。2.2 标识符命名、运算符优先级、枚举与数组越界标识符命名规则属于基础中的基础但每次都有不少人丢分。合法的Java标识符由字母、数字、下划线_、美元符号$组成且不能以数字开头不能是关键字。容易被忽略的点有两个$是合法的标识符字符以及中文也是合法的字母字符——所以String 名字 test在Java里其实能编译通过。笔试碰到这种题千万别凭直觉说中文不行。运算符优先级考的最多的就是、三目运算符和赋值运算的组合。比如int x 5; int y x x;问y是多少。这种题要一步步算x先取5x变成6x先加再取x是7取7所以y等于12x最终是7。枚举类型是游戏开发中常用的语法笔试如果考到多半会和switch搭配。注意几个细节枚举类本质上继承自java.lang.Enum不能手动new枚举常量必须在第一行声明values()方法返回所有枚举常量枚举可以定义字段、构造器和普通方法。举例enum PlayerState { IDLE(0), RUNNING(1), ATTACK(2); private final int code; PlayerState(int code) { this.code code; } public int getCode() { return code; } }这种写法在游戏的状态机逻辑里很常见笔试偶尔会让你补充代码或者判断题目的输出。数组越界异常ArrayIndexOutOfBoundsException本身是运行时异常笔试常考的点在于数组下标从0开始、length是属性不是方法、增强for循环内部用的是迭代器或下标遍历、二维数组的length只表示第一维长度。代码中常见的越界场景是循环条件写成了i arr.length这会多访问一次越界位置。平时写代码时养成用i arr.length的习惯能少踩很多坑。2.3 读代码题的一个高效复盘思路做这类题时我的习惯是先判断代码里涉及了哪些“机制点”再逐个在脑子里模拟执行。一个输出题如果同时包含了静态块、异常、finally、继承那就把它拆成几个独立机制分别推演最后合并。千万不要从头到尾一口气读到底那样很容易漏掉某个执行顺序的细节。刷题的时候多留意这类综合题比做十道单考点题更有用。3. 集合框架与常用类HashMap、排序比较器是笔试常客3.1 HashMap的底层结构和扩容逻辑集合框架的题在笔试中占比不低而HashMap在集合题中的地位是绝对的C位。JDK 8之后的HashMap底层结构是“数组 链表 红黑树”当链表长度达到8且数组长度达到64时链表会转为红黑树当红黑树节点数小于6时会退回链表。这个数字很多人会背但笔试更可能考的是扩容机制默认初始容量是16负载因子是0.75当元素数量超过容量 × 负载因子时触发扩容扩容后容量变为原来的两倍。这里有个细节值得注意HashMap允许key和value为nullnull key会存放在下标为0的桶里。而Hashtable不允许null因为它的put方法会直接调用hashCode()null会抛NullPointerException。如果题目以选择题形式出现这就是快速排除错误选项的线索。线程安全方面HashMap不是线程安全的并发场景下应该使用ConcurrentHashMap。笔试偶尔会问ConcurrentHashMap是如何保证线程安全的——JDK 8之后是利用CAS synchronized对每个桶加锁锁粒度细了并发性能比JDK 7的Segment分段锁更好。如果题目时间充裕可以写一句“在对单个桶操作时才会触发synchronized”这句话能体现你真的理解过底层。3.2 ArrayList和扩容细节ArrayList是面试里常被追问、笔试里也容易出现的类。它的默认初始容量是10JDK 7之后懒加载第一次add才初始化扩容时新容量是旧容量的1.5倍也就是旧容量 旧容量 1底层用Arrays.copyOf拷贝到新数组。注意这里有个容易混淆的点ArrayList扩容是1.5倍HashMap扩容是2倍两者别搞混。另外Arrays.asList()返回的List是一个定长List不能调用add或remove否则会抛UnsupportedOperationException。这个点经常出现在判断题里看着像能改实际不能。3.3 Comparator.comparing的排序写法排序类题目在游戏后端笔试中很常见因为排行榜就是典型的排序场景。Java 8之后最常用的排序写法就是Comparator.comparing。比如有一个玩家类class Player { private String name; private int level; private long score; // 省略getter/setter }按分数降序排列list.sort(Comparator.comparing(Player::getScore).reversed());按等级升序、分数降序list.sort(Comparator.comparing(Player::getLevel) .thenComparing(Comparator.comparing(Player::getScore).reversed()));热搜词里有一条“将某元素值放第一个”的写法这在实际开发里也经常遇到——比如置顶VIP玩家。可以这样写list.sort(Comparator.comparing((Player p) - p.isVip() ? 0 : 1) .thenComparing(Player::getLevel, Comparator.reverseOrder()));用Comparator.comparing传一个排序键再用thenComparing串联次要排序键整个代码既清晰又不易出错。笔试如果遇到排序题优先用这种写法比手动写Comparator匿名内部类要简洁得多——后者一旦排序规则复杂代码可读性就会断崖式下降。3.4 集合中容易被忽视的低频考点HashSet的底层其实是一个HashMap它把元素存在key的位置value统一是一个固定的Object对象所以HashSet不能有重复元素。TreeSet基于TreeMap底层是红黑树插入元素时会自动排序所以元素必须实现Comparable接口或者在构造时传入Comparator否则运行时会抛ClassCastException。这几个点选择题偶尔会出现知道底层实现就不会做错。4. 手写排序算法冒泡和快排在笔试中的标准答法4.1 冒泡排序别只会写最朴素的版本手写冒泡排序几乎是校招笔试的标配题但这道题想得高分不能只写出最基础的版本最好能体现出优化意识。最原始的冒泡排序public static void bubbleSort(int[] arr) { for (int i 0; i arr.length - 1; i) { for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; } } } }这个写法没有任何问题但可以在内层循环加一个标记位如果某一轮没有发生交换说明数组已经有序直接退出外层循环public static void bubbleSort(int[] arr) { for (int i 0; i arr.length - 1; i) { boolean swapped false; for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) { break; } } }笔试阅卷时这个优化绝对是加分项因为说明你思考过“最好情况下时间复杂度可以降到O(n)”这件事而不是死记硬背一套模板。4.2 快速排序边界条件是得分关键快速排序是笔试手写题里最容易翻车的算法主要翻在边界条件和递归出口上。标准写法有很多种我推荐最容易写对的双指针交换法public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot arr[left]; int i left; int j right; while (i j) { while (i j arr[j] pivot) { j--; } while (i j arr[i] pivot) { i; } if (i j) { int tmp arr[i]; arr[i] arr[j]; arr[j] tmp; } } arr[left] arr[i]; arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }这里有两个关键点一是外层while的终止条件是i j而不是i j二是内层两个while一定要附带i j的短路判断否则会出现数组越界。我见过很多人栽在第二点上——内层循环没有加i j导致j一路左移直接冲出左边界。此外要注意如果数组已经是升序排好的上面的写法每次选第一个元素做基准会导致左右严重失衡退化到O(n²)。笔试虽然不要求你实现三数取中法但如果能在注释里提一句“可以做三数取中优化”阅卷印象分绝对不会差。4.3 排序算法对比表算法平均时间复杂度最坏时间复杂度空间复杂度稳定性冒泡排序O(n²)O(n²)O(1)稳定快速排序O(n log n)O(n²)O(log n)不稳定归并排序O(n log n)O(n log n)O(n)稳定堆排序O(n log n)O(n log n)O(1)不稳定这道题如果出现在笔试里通常不会只让你写排序算法本身还会顺带问一句“这个算法的稳定性如何”。记住一个判断思路如果有相隔较远的元素发生交换大概率不稳定如果只是相邻元素两两比较交换通常稳定。冒泡是稳定排序快排是不稳定排序。5. JVM内存与异常机制OOM和数组越界背后的原理考察5.1 OutOfMemoryError到底意味着什么热搜词里出现了java: outofmemoryerror: insufficient memory说明不少人在实际运行Java程序时踩过这个坑。OOM的全称是OutOfMemoryError它不是一个Exception而是一个Error——直接继承自Throwable正常情况下不应该去捕获它。JVM运行时数据区中最容易触发OOM的是堆内存。如果创建对象的速度超过GC回收的速度堆内存爆满后就会抛java.lang.OutOfMemoryError: Java heap space。另外还有几种常见的OOM变体比如元空间不足会抛Metaspace OOM创建线程时本地内存不足会抛unable to create new native thread。笔试选择题如果考OOM大概率会问下面几种情况的对应关系报错信息触发原因常见场景Java heap space堆内存不足大对象过多、内存泄漏Metaspace元空间不足动态生成类过多unable to create new native thread无法创建本地线程线程数超限GC overhead limit exceededGC回收效率过低几乎耗尽CPU在GC上如果笔试考到OOM的排查思路可以写先通过jmap -dump导出堆转储文件再用MAT或VisualVM分析堆中哪些对象占比最高找到内存泄漏点。这一步即使只写思路也足够了阅卷人看的是你有没有接触过真实线上问题的排查。5.2 栈内存溢出与递归的经典搭配StackOverflowError也是笔试常客它的触发原因很纯粹——线程请求的栈深度超过了JVM允许的深度最常见的写法就是无终止条件的递归。题目经常会给出一个递归函数比如public int factorial(int n) { return n * factorial(n - 1); }问运行结果。答案就是StackOverflowError因为递归没有出口栈帧不断压入最终撑爆线程栈。这里可以补充一点栈的大小是可以通过-Xss参数调整的但增大栈大小只会推迟栈溢出的发生不能根治无限递归。5.3 受检异常和非受检异常的区别异常体系是Java笔试里必考的基础题。受检异常Checked Exception必须显式捕获或声明抛出常见的有IOException、SQLException。非受检异常RuntimeException不需要强制捕获常见的有NullPointerException、IndexOutOfBoundsException、ClassCastException、IllegalArgumentException。Error则代表系统级错误比如前面的OOM和StackOverflowError。笔试经常用如下写法来迷惑考生Thread.sleep(1000);这行代码不加try-catch或throws是编译不过的因为InterruptedException是受检异常。很多人在IDE里习惯了AltEnter自动补try-catch笔试时反而会忽略这个特性。5.4 finally的执行时机和特例final和finally的区别是送分题但finally的执行时机才是真正的考点。标准答案是只要try代码块执行了finally就一定会执行——除非在try代码块中调用了System.exit()或者JVM崩溃。还有一种特例需要特别注意如果在try里执行了returnfinally也会在return之前执行。public static int test() { int x 1; try { return x; } finally { x 2; } }上面这个方法返回的是1而不是2。因为return x时先把x的值1放到返回值槽里然后才执行finallyfinally中修改x不会影响已经保存的返回值。但如果你在finally里直接return则会覆盖try里的return值——这种代码可读性极差笔试却特别喜欢当作陷阱题来考。5.5 try-with-resources的新写法既然提到了异常顺带说一句JDK 7开始支持的try-with-resources。写法是用try (资源对象)资源必须实现AutoCloseable接口无论是否抛异常资源都会自动关闭。笔试如果考文件操作相关的异常处理用这种写法会比老式的try-catch-finally更符合现代Java风格try (BufferedReader reader new BufferedReader(new FileReader(config.txt))) { String line reader.readLine(); } catch (IOException e) { e.printStackTrace(); }如果考场上遇到这种题优先往try-with-resources方向靠这是一个明显的加分点。6. Java 8特性与Lambda新特性题目怎么拿高分6.1 Lambda表达式和函数式接口2019年的校招笔试已经大量出现Java 8特性的题目到现在Lambda更是成了默认要求。Lambda本质上是函数式接口的简洁实现。所谓函数式接口就是只包含一个抽象方法的接口在Java中会用FunctionalInterface注解标注。一个典型的匿名内部类写法Runnable task new Runnable() { Override public void run() { System.out.println(old style); } };用Lambda改写后Runnable task () - System.out.println(new style);笔试可能考的不是怎么把匿名类改成Lambda而是给一段Lambda代码问它对应的是哪个函数式接口。这里需要熟悉JDK自带的四个核心函数式接口接口抽象方法用途FunctionT, RR apply(T t)接收一个参数返回一个结果Consumervoid accept(T t)接收一个参数无返回结果Predicateboolean test(T t)接收一个参数返回booleanSupplierT get()无参数返回一个结果比如list.forEach(item - System.out.println(item))中的Lambda对应Consumer接口因为forEach方法期望的参数就是Consumer。6.2 Stream API的常见考点Stream是另一个高频考点。笔试最常见的范围是给定一个集合要求用Stream实现过滤、转换、聚合。比如有一个整数列表要选出其中的偶数并求出它们的平方和ListInteger numbers Arrays.asList(1, 2, 3, 4, 5, 6); int sum numbers.stream() .filter(n - n % 2 0) .mapToInt(n - n * n) .sum(); System.out.println(sum); // 56这里有几个常考细节filter是惰性求值只有在遇到终止操作比如sum、collect、forEach时才真正执行Stream是一次性使用对象用完之后不能重复消费map和flatMap的区别——map是一对一转换flatMap是把每个元素转换成多个元素后拍扁。笔试偶尔会用flatMap做多级集合的合并比如ListListString转ListString。另一个容易被问到的考点是collect(Collectors.toList())。面试的时候会追问能不能用Collectors.toUnmodifiableList()这是JDK 17之后才有的但笔试试卷一般以JDK 8为主写到toList()就足够了。6.3 方法引用和常见异常方法引用是Lambda的简化写法笔试一般只要求识别而不是要求写。比如list.forEach(System.out::println)这等价于list.forEach(item - System.out.println(item))。理解了这一点读代码题就不会被这种写法吓到。另外还有个高频坑在Lambda内部使用外部变量时外部变量必须是final或事实final的。比如下面这段代码编译会报错int multiplier 2; list.forEach(n - System.out.println(n * multiplier)); multiplier 3; // 编译报错Lambda引用的局部变量必须是final或事实final这是因为Java对Lambda的变量捕获有严格限制。笔试如果给一段类似的代码问能否编译通过答案就是不能。7. 笔试实战复盘结合这套题我的备考建议7.1 时间分配建议选择、读代码、手写题搜狐畅游这套题虽然我没有拿到完整原卷但从题型特征来看和市面上大部分校招笔试的节奏是接近的。我的建议是选择判断题不要纠结超过2分钟一道不会的先标记跳过读代码题按我前面说的“拆机制点”的思路逐项推演手写题优先写最熟悉的排序算法写完一定要检查数组边界。手写代码时哪怕代码逻辑不够完美也尽量保持缩进和命名清晰因为笔试阅卷有可能有人工环节代码风格会影响印象分。变量名用arr、left、right这种显而易见的不要用a、b这种毫无信息量的。7.2 刷题时最容易忽略的“失分区”我从这些年的经验来看校招选手最容易失分的不是难题而是三类低级错误。第一类是自动装箱的陷阱Integer a 127; Integer b 127;用比较返回true但如果值是128返回就是false——因为Integer缓存范围是-128到127。第二类是字符串常量池问题String s1 abc; String s2 new String(abc);用比较不相等但s1.equals(s2)返回true。第三类就是我前面反复强调的finally和return的执行顺序。这三类题不存在“不会做”只存在“考场上一时没想到”。解决办法很简单——考前三天把历年真题里的读代码题全部过一遍认认真真在纸上写出每一步的执行结果比背十遍概念都管用。7.3 一个值得长期坚持的复盘习惯笔试结束后我建议你把做错的每道题整理成一个小卡片正面写题目核心考点背面写错误原因。你会发现大多数错误可以归为两类概念记得不牢或者执行顺序推演错误。前者去翻官方文档后者直接在IDE里跑一遍观察真实输出然后反复推演“为什么输出是这样”。一句话笔试考的不是你会不会而是你稳不稳。最后再分享一个我个人的体会刷题和背八股的根本目的不是让你记住某个具体答案而是让你在面对一个陌生问题的时候敢于顺着Java语言本身的规则一步步推理出结果。这个能力养成了不管是笔试、面试还是进入游戏公司接触真实业务都会让你走得比同龄人更稳当。