ARTICLE DETAIL

资讯详情

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

Java刷题集合与类型转换实战:ArrayList、HashMap高频避坑指南

Java刷题集合与类型转换实战:ArrayList、HashMap高频避坑指南 刷题最崩溃的瞬间是什么不是思路想不出来而是思路全对、代码却编译不过。List 转int[]写错了Arrays.asList把整个int[]当成了一个元素Integer用比较在两数之和里莫名其妙返回false……这些坑我在刚开始刷题的时候几乎每场笔试都要踩一遍。Java集合框架和类型转换看起来是八股文里的老生常谈但在OJ环境里它们直接决定你的代码能否通过。这篇文章只聊刷题场景下的集合与类型转换实战哪些容器该无脑选数组和List互转有哪些写法高频题里怎么用HashMap和优先队列。适合正在刷力扣、准备笔试面试的Java选手也适合那些“API都知道、一到OJ就编译错误”的朋友。后面所有代码都用最贴近笔试的写法不整花活拿去就能用。1. 为什么刷题翻车十次有八次死在类型转换上先接受一个事实刷题和你平时写业务代码完全是两种思维模式。业务代码里类型错了IDE飘红、编译器提示、代码评审也可能拦住你OJ环境里只有一次Submit编译错误直接判罚时没有解释机会。而且判题系统的方法签名是定死的比如题目要求返回int[][]你就必须返回int[][]返回一个ArrayListint[]哪怕逻辑全对结果也是零分。1.1 OJ环境与业务代码的三个关键差异第一刷题基本都是“方法级”编程。你只需要实现一个方法输入由系统解析好了传进来输出靠返回值或标准输出。方法签名一旦写错整个类编译不过。第二OJ的测试往往是大规模数据集合选错实现类会产生肉眼可见的超时差异。比如用一个LinkedList做随机访问数据规模到10^5级别可能还勉强能撑到10^6就等着TLE。第三判题系统对输出类型极严格题目要int[]你返回List 就算内容一模一样也是编译错误没有类型自动转换的空间。所以刷题这件事“会用集合”还不够得“在限时高压下不用查资料就能写出正确类型转换”才算过关。这也是为什么很多人把集合和类型转换归到Java基础里但面试官依然爱问、笔试依然爱考——它们直接决定了代码能不能跑。1.2 一次典型的翻车现场复盘我拿自己以前踩过的三个真实坑当例子给大家看看到底错在哪。第一个坑int[]和Arrays.asList。很多人写List list Arrays.asList(new int[]{1, 2, 3});结果编译通过但运行起来完全不是想要的。因为泛型只能是引用类型int[]作为一个整体对象被放进List里得到的是Listint[]长度是1不是3。想快速转得先把int[]变成Integer[]或者用Stream的boxed()方法。第二个坑toArray参数。List 转int[]很多人写list.toArray(new Integer[0])看着没问题但题目要int[]不是Integer[]。两个类型在OJ里完全不能互相赋值必须再手动循环拆箱或者用stream的mapToInt。反过来Listint[]转int[][]写res.toArray(new int[0][])这里的new int[0][]不是“创建一个空二维数组塞进去”而是给toArray一个“类型提示”实际结果由虚拟机按list.size()生成。第三个坑包装类的比较。两数之和里if (map.get(complement) ! i)这种写法在值大于127时可能因为Integer缓存问题得到错误结果。-128到127之间Integer会复用缓存对象比较碰巧成立超出范围就不成立了。刷题里遇到包装类型比较无脑用equals或直接用intValue()别赌缓存。这三个坑有个共同特点它们不是算法题是语言基础题。而语言基础恰恰是临场最容易崩的地方。1.3 类型转换为什么在刷题时特别容易翻车根本原因还是Java的类型体系严谨得让人头大。数组是一种特殊的引用类型但不是集合不能调用集合方法List是接口ArrayList是实现类int[]和Integer[]不是一回事String内部是char[]但不是char[]。这些“看似相关、实则不同”的类型在刷题时高频交错出现稍不注意就错。再加上泛型擦除这个特性。泛型只是在编译期做类型检查运行时List 和List 在JVM眼里都是List取出的元素永远被当作Object处理。于是你写完List raw new ArrayList();编译器会报unchecked警告运行时还可能ClassCastException。刷题时为了省事不写泛型无异于给自己埋雷。知道自己会在哪摔倒后面就好办了。接下来先把集合框架的选型一次性理清。2. 集合框架刷题前必须理清的继承树很多人一提Java集合就头大觉得体系庞大。其实刷题根本用不着背全景只需要抓住一条主线Collection是根接口List、Set、Queue三类接口从它extends出来Map是独立的另一个体系。代码里99%的操作都发生在ArrayList、HashMap、HashSet、PriorityQueue这几个实现类上。2.1 四个核心接口半分钟认清体系记忆方法Collection只管一堆元素不管是重复还是不重复List是有序、可重复的“队列”Set是逻辑上无重复的“篮子”Queue是带存取规则的“队”常配合BFS使用。Map是键值对的“字典”不属于Collection因为它存的是Pair不是一个一个孤立的元素。判断接口用什么类型就看题目需要什么语义要保序可重复就List要去重就Set要查表就Map要先进先出就Queue。2.2 高频实现类选型对照表我整理了一张刷题版选型表场景推荐实现类时间复杂度刷题理由有序列表、随机访问ArrayListget是O(1)遍历最快几乎无脑选频繁头尾插入当栈/队列用ArrayDeque均摊O(1)比LinkedList的队列实现更省内存更快去重、判重HashSet均摊O(1)布尔语义最强“这个值见过吗”保持插入顺序的去重LinkedHashSet均摊O(1)需要“去重且按首次出现顺序输出”时用键值查表、计数HashMap均摊O(1)刷题第一神器没有之一保持插入顺序的键值对LinkedHashMap均摊O(1)需要按插入顺序输出时用按key自然排序的键值对TreeMapO(log n)红黑树实现刷题低频但面试爱问TopK、堆PriorityQueue插入/删除O(log n)默认小顶堆求前K大直接开堆在大多数笔试里这套选型表能覆盖九成题目。我个人的习惯是没有特殊需求就不用LinkedList、TreeMap、TreeSet这些“特种部队”不是因为它们不好而是ArrayListHashMapHashSet已经能从最简单的问题撑到最复杂的题减少临场思考成本。2.3 泛型刷题中怎么用才能少些编译错误泛型的核心作用是编译期类型检查。写了MapString, List map new HashMap()之后编译器能在你put错类型的时候立刻报错而不是运行到一半才抛ClassCastException。刷题时这种“把错误挡在编译期”的能力特别宝贵因为OJ只会告诉你编译错误不会告诉你哪行错了。两点实操建议。第一等号右边可以只写菱形语法省事且语义清楚。第二返回集合时尽量返回接口类型比如List 而不是ArrayList 这样方法签名更通用方便复用。但变量声明处如果要用到ArrayList特有方法才写具体类。有一个细节容易被忽略数组可以作为泛型实参。Listint[]完全合法因为int[]是引用类型在泛型里它和Integer地位一样。合并区间题目里Listint[] int[][]互转是标准打法后面会专门演示。3. 类型转换刷题高频基础操作全解类型转换不是“会转就行”而是要能选出最短、最不易错的那条路。我按刷题出现频率把必背操作拆成四块讲。3.1 数组与List互转的四种写法先说明一个大前提基本类型数组int[]和包装类型数组Integer[]在转换时的待遇完全不同。Arrays.asList只认引用类型数组。写法一String[] / Integer[]转List。List list Arrays.asList(strArr);。返回的List由固定大小数组支撑不能add/remove但可以set替换元素。如果想真正可变再包一层new ArrayList(Arrays.asList(strArr))。写法二int[]转List 。一条流式写法是Arrays.stream(nums).boxed().collect(Collectors.toList())。boxed()的作用是把IntStream里的每个int打包成Integer再收集进List这也是我推荐的“一行流”。如果嫌stream有点绕最朴素的for循环加list.add(nums[i])也完全没问题笔试能用就行。写法三List 转int[]。for循环手动拆箱是最稳的兼容任何Java版本。int[] res new int[list.size()]; for (int i 0; i list.size(); i) { res[i] list.get(i); }。一行流则是list.stream().mapToInt(Integer::intValue).toArray()。这两种我都实测过后者写起来快但笔试紧张时方法引用容易写错求稳直接for循环。写法四List 转String[]。list.toArray(new String[0])是标准写法。传入new String[0]不是要“塞进空数组”而是告诉JVM最终要生成的数组类型。Effective Java里也推荐用0长度数组因为简单且能避免预先分配错误大小。这个写法在Java 8和11下表现都很好。3.2 数组/List与Set互转去重是刷题里的高频需求。最简单的是new HashSet(list)一条语句完成“去重并转Set”。数组转Set时包装类型数组可以直接HashSet(Arrays.asList(arr))int[]就需要Stream先boxed再collect到Set。反过来Set转List就是new ArrayList(set)Set转数组可以先转List再toArray或者直接set.toArray(new Integer[0])不过拿到的还是Integer[]要int[]还得循环。有一道经典题是“数组中重复的数字”用HashSet边遍历边查第一次遇到的数加进去第二次遇到直接返回这就是HashSet最常见的刷题姿势。3.3 String与char[]、int[]的转换字符串操作在刷题里非常高频至少要会这几组s.toCharArray()String转char[]几乎每个字符串题第一步。String.valueOf(chars)或new String(chars)char[]转String字母异位词分组题里当key用。s.split( )按空格分割成String[]注意split参数是正则目的字符如果是点号要转义。int[] cnt new int[26]; char c s.charAt(i); cnt[c - a]统计字母出现次数的惯用法基于字符编码连续。char到intint n c - 0char 5减0得到5。整数字符串到数字Integer.parseInt(123)。这里有个小技巧如果要把字符串“abc”变成字符集计数不需要转成List直接cnt[s.charAt(i) - a]就行。刷题时能用数组别用集合数组更快代码也更短。3.4 基本类型与包装类型的自动装箱拆箱陷阱Java编译器会在基本类型和包装类型之间自动转换这是语法糖。但封装在背后的开销和语义陷阱刷题时经常爆雷。第一个雷性能。大量装箱拆箱在循环里会有额外对象创建开销不过刷题的数据规模一般没到必须抠这个的程度但如果你在10^6级别的循环里反复new IntegerGC压力还是肉眼可见的。我的习惯是循环内尽量用基本类型int、long需要入集合时才装箱。第二个雷比较。包装类型的比较的是对象引用而Integer从-128到127有缓存池。if (Integer.valueOf(127) Integer.valueOf(127))为true换成128就是false。这不是bug是设计但刷题时就是坑。两个Integer要比较值统一用equals()或intValue()。第三个雷自动装箱发生在赋值和方法传参时。比如map.put(key, i)里的i自动装箱了map.get(key)返回的是Integer把它赋给int j时又自动拆箱。如果中间出现null拆箱会抛NullPointerException。刷题时getOrDefault能避免大部分空指针但还是要时刻记得返回值可能是null。4. 集合实战三道高频题带你打通类型转换光说不练假把式。我挑了三道非常典型的力扣题按“从简单到组合”的顺序演示集合与类型转换怎么落地。4.1 两数之和HashMap消除嵌套循环题目给定int[] nums和一个目标值target返回两个下标使两数之和等于target。暴力法是双重循环O(n^2)用HashMap能压到O(n)public int[] twoSum(int[] nums, int target) { MapInteger, Integer map new HashMap(); for (int i 0; i nums.length; i) { int diff target - nums[i]; if (map.containsKey(diff)) { return new int[]{map.get(diff), i}; } map.put(nums[i], i); } return new int[0]; }关键点map的key存“数组元素值”value存“下标”。因为题目只要求一组答案所以遍历到某个数时只要之前存在target - nums[i]直接返回。这里用containsKey而不是get后判null是更清晰的做法。最后一行return new int[0]不是摆设Java要求方法必须有返回值返回null在OJ里可能被接受但返回空数组更安全调用方不会踩空指针。4.2 字母异位词分组List与Map的组合拳题目字符串数组分组字母异位词归为同一组。经典思路是“排序后字符串作为key”public ListListString groupAnagrams(String[] strs) { MapString, ListString map new HashMap(); for (String s : strs) { char[] chars s.toCharArray(); Arrays.sort(chars); String key String.valueOf(chars); map.getOrDefault(key, new ArrayList()).add(s); } return new ArrayList(map.values()); }等等上面这个写法有个坑getOrDefault返回的是List如果key不存在getOrDefault返回一个新建的空白List并add了s但这个新List没有放回map里下一次相同key的字符串进来map里还是没有这个key又会新建一个List导致分组丢失。正确写法是public ListListString groupAnagrams(String[] strs) { MapString, ListString map new HashMap(); for (String s : strs) { char[] chars s.toCharArray(); Arrays.sort(chars); String key String.valueOf(chars); ListString list map.getOrDefault(key, new ArrayList()); list.add(s); map.put(key, list); } return new ArrayList(map.values()); }必须先取出Listadd之后再put回去。或者用map.computeIfAbsent(key, k - new ArrayList()).add(s)更简洁但JDK 8才行。这道题把String转char[]、排序、再转回String、用MapString, List 聚合、最后map.values()转ListList 类型转换和集合组合的考点全占齐了非常值得反复做。4.3 合并区间Listint[]和int[][]的往返题目给定若干区间合并有重叠的。这道题的输出类型是int[][]中间过程最舒服的容器是Listint[]public int[][] merge(int[][] intervals) { if (intervals.length 0) return new int[0][0]; Arrays.sort(intervals, (a, b) - a[0] - b[0]); Listint[] res new ArrayList(); int start intervals[0][0]; int end intervals[0][1]; for (int i 1; i intervals.length; i) { if (intervals[i][0] end) { end Math.max(end, intervals[i][1]); } else { res.add(new int[]{start, end}); start intervals[i][0]; end intervals[i][1]; } } res.add(new int[]{start, end}); return res.toArray(new int[0][]); }第一个看点Arrays.sort(intervals, (a, b) - a[0] - b[0])对二维数组按第一列升序排列。第二个看点res.toArray(new int[0][])这个写法在前面的toArray参数问题里出现过Listint[]转int[][]只能用这种“给类型提示”的方式。你直接写res.toArray()会得到Object[]根本无法返回int[][]。很多人在这个转换上卡住其实记住“new T[0]作为参数传给toArray”这个套路就够了。这道题还有一个常见变体求区间交集。同样是用Listint[]存结果最后toArray(new int[0][])收尾框架完全一样改判断条件即可。5. 刷题高频场景排序、去重、TopK一把梭刷题有三大“模板化场景”排序、去重、TopK。这三个场景几乎每轮笔试都出现我总结成一套可以直接抄的姿势。5.1 排序的三种姿势与比较器写法Java里排序入口就两个Arrays.sort用于数组Collections.sort用于List。写比较器有四个要点第一数组排序可以直接Arrays.sort(arr)int[]、String[]都行String[]默认字典序。第二List排序用Collections.sort(list)或list.sort(null)按自然序都行。第三复杂对象排序要自己写Comparator推荐Lambda写法(a, b) - a[1] - b[1]表示按第二元素升序想降序就反过来b[1] - a[1]。第四比较int值用Integer.compare(a, b)更安全用减法在极端值会溢出。例如(a, b) - a[1] - b[1]当a[1]很大b[1]很小时会溢出虽然刷题数据一般碰不到但面试官会盯着这个点问。优先级队列的排序规则同理new PriorityQueue((a, b) - a[1] - b[1])就是按第二元素的小顶堆。这里建议用Comparator.comparingInt(a - a[1])更可读也避免溢出。5.2 去重从HashSet到Stream distinct去重最常用的三招。第一招是new HashSet(list)顺手完成去重如果还需要保持原顺序用LinkedHashSet。第二招是边遍历边判断Set seen new HashSet(); if (!seen.add(num))add方法返回boolean重复元素add会返回false这个写法在“找第一个重复数字”的题里特别好用。第三招是Java 8的streamlist.stream().distinct().collect(Collectors.toList())代码最短但笔试环境里理论上可用实际我很少用它因为distinct背后还是LinkedHashSet性能和可读性都不如直接用Set。5.3 TopK问题PriorityQueue的默认小顶堆TopK是个高频考点。核心结论求前K大用小顶堆求前K小用大顶堆。PriorityQueue默认是小顶堆堆顶是最小元素。求前K大时遍历元素堆没满就加堆满了如果新元素比堆顶大就poll出堆顶再add新元素这样堆里始终保留当前看到的最大K个。求前K小就用大顶堆构造时写new PriorityQueue(Comparator.reverseOrder())。前K个高频元素的经典流程是先用HashMap统计次数再把entry扔进按value排序的sizeK的小顶堆最后poll出来。这条路把HashMap、PriorityQueue、Comparator全串起来了建议用手写一遍。有个细节PriorityQueue比较的是“元素本身”所以存Map.Entry时写成(a, b) - Integer.compare(a.getValue(), b.getValue())。或者用Comparator.comparingInt(Map.Entry::getValue)一行搞定。6. 常见坑位与排查技巧实录最后一部分上干货中的干货我在刷题和真实笔试里撞过的坑以及排查思路整理成速查手册。6.1 ConcurrentModificationException边遍历边删除在foreach循环里调用list.remove()运行时会抛ConcurrentModificationException。原因是foreach展开成Iterator每次next()都会校验modCountremove会修改modCount两者不一致直接爆发。解决有三个方向第一改用Iterator用it.remove()这是Iterator接口自带的安全删除方法第二用list.removeIf(predicate)JDK 8一条语句搞定第三先把要删的对象收集到一个List遍历完了统一removeAll。刷题里最省事的是removeIf但面试八股问到原理你得能讲出modCount这层原因。6.2 equals与hashCodeHashMap的key为何总出问题HashMap查key时先算hashCode定位桶再用equals找具体元素。如果一个自定义类只重写equals不重写hashCode两个对象明明逻辑相等却可能哈希到不同桶get直接返回null。刷题里自定义对象当key的情况不多但一旦出现比如坐标点、状态封装必须两个方法一起重写。偷懒技巧是Objects.hash(x, y)生成hashCode。另外如果不想破坏可变性尽量用不可变对象当key比如String、Integer否则put之后又改key内容后面get会找不到。6.3 Integer缓存128这个数字有毒前面多次提到这里集中说一下。Integer.valueOf()会缓存-128到127的实例自动装箱底层调用的就是valueOf。所以Integer a 127; Integer b 127; a b是trueInteger a 128; Integer b 128; a b是false。同时new Integer(127)特意new了两个不同对象也是false。缓存机制只对valueOf和自动装箱生效。刷题写比较包装类型时建议一律用equals至少别在这个细节上丢分。6.4 刷题前必会API清单我整理了一个“考前30秒扫一眼”清单操作推荐写法易错点int[]转ListArrays.stream(nums).boxed().collect(Collectors.toList())不能用Arrays.asList(int[])List 转int[]list.stream().mapToInt(Integer::intValue).toArray()别忘了toArrayList 转T[]list.toArray(new T[0])参数是类型提示不是容量Listint[]转int[][]res.toArray(new int[0][])不能直接res.toArray()String转char[]s.toCharArray()结果不能直接用字符串拼接char[]转StringString.valueOf(chars)new String(chars)也可以Map取默认值map.getOrDefault(key, defaultValue)注意defaultValue别每次新建list导致分组丢失TopK用小顶堆PriorityQueue pq new PriorityQueue()默认就是小顶堆字母计数cnt[s.charAt(i) - a]前提是连续小写字母每次笔试前把这张表过一遍能减少不少低级错误。实战里还有一个特别容易忽略的心态问题遇到类型转换卡住别慌先回头看看方法签名再看看题目给的输入输出类型往往答案就在那里。代码写错不可怕可怕的是不知道错在哪、不知道怎么通过系统性的API记忆去规避。我个人刷下来的最大体会是集合与类型转换就是Java刷题的“肌肉记忆”。算法思路可以慢慢想但API调用必须变成条件反射看到“返回int[][]”就想到Listint[]和toArray看到“去重”就想到HashSet看到“找前K”就想到小顶堆。这些反射建立起来之后笔试的时间会宽裕很多真正留给思考的空间也更多。最后再分享一个小技巧刷完每道题不管AC没AC花十秒钟把题里用到的集合和类型转换写进自己的错题本。比如“合并区间Listint[] toArray(new int[0][])”“两数之和HashMap containsKey”。下次遇到类似题你根本不需要现场回忆API抄作业就行。这个小习惯让我的笔试速度提升了一个档次强烈建议你也试试。
返回列表