前言 本篇分析的技巧点其实是比较常见的,但是最近的几次的代码评审还是发现有不少兄弟没注意到。 所以还是想拿出来说下。正文 是个什么场景呢? 就是for循环里面还有for循环,然后做一些数据匹配、处理这种场景。 我们结合实例代码来看看。 场景示例:比如我们现在拿到两个list数据,一个是UserList集合;另一个是UserMemoList集合; 我们需要遍历UserList,然后根据userId从UserMemoList里面取出对应这个userId的content值,做数据处理。 代码User。java:importlombok。Data;DatapublicclassUser{privateLonguserId;privateStringname;} 代码UserMemo。java:importlombok。Data;DatapublicclassUserMemo{privateLonguserId;privateStringcontent;} 模拟数据集合: 5W条user数据,3W条userMemo数据publicstaticListUsergetUserTestList(){ListUserusersnewArrayList();for(inti1;i50000;i){UserusernewUser();user。setName(UUID。randomUUID()。toString());user。setUserId((long)i);users。add(user);}returnusers;}publicstaticListUserMemogetUserMemoTestList(){ListUserMemouserMemosnewArrayList();for(inti30000;i1;i){UserMemouserMemonewUserMemo();userMemo。setContent(UUID。randomUUID()。toString());userMemo。setUserId((long)i);userMemos。add(userMemo);}returnuserMemos;} 先看平时大家不注意的时候可能会这样去写代码处理: ps:其实数据量小的话,其实没多大性能差别,不过我们还是需要知道一些技巧点。 代码:publicstaticvoidmain(String〔〕args){ListUseruserTestListgetUserTestList();ListUserMemouserMemoTestListgetUserMemoTestList();StopWatchstopWatchnewStopWatch();stopWatch。start();for(Useruser:userTestList){LonguserIduser。getUserId();for(UserMemouserMemo:userMemoTestList){if(userId。equals(userMemo。getUserId())){StringcontentuserMemo。getContent();System。out。println(模拟数据content业务处理。。。。。。content);}}}stopWatch。stop();System。out。println(最终耗时stopWatch。getTotalTimeMillis());} 我们来看看这时候的一个耗时情况: 相当于迭代了5W3W次 可以看到用时是26857毫秒 其实到这,插入个题外点,如果说每个userId在UserMemoList里面都是只有一条数据的场景。for(Useruser:userTestList){LonguserIduser。getUserId();for(UserMemouserMemo:userMemoTestList){if(userId。equals(userMemo。getUserId())){StringcontentuserMemo。getContent();System。out。println(模拟数据content业务处理。。。。。。content);}}} 单从这段代码有没有问题,有没有优化点。 显然是有的,因为当我们从内循环UserMemoList里面找到匹配数据的时候,没有做其他操作了。 这样内for循环会继续下,直到跑完再进行下一轮整体循环。 所以,仅针对这种情形,1对1的或者说我们只需要找到一个匹配项,处理完后我们应该使用break。 我们来看看加上break的一个耗时情况: 代码:publicstaticvoidmain(String〔〕args){ListUseruserTestListgetUserTestList();ListUserMemouserMemoTestListgetUserMemoTestList();StopWatchstopWatchnewStopWatch();stopWatch。start();for(Useruser:userTestList){LonguserIduser。getUserId();for(UserMemouserMemo:userMemoTestList){if(userId。equals(userMemo。getUserId())){StringcontentuserMemo。getContent();System。out。println(模拟数据content业务处理。。。。。。content);break;}}}stopWatch。stop();System。out。println(最终耗时stopWatch。getTotalTimeMillis());} 耗时情况: 可以看到从2W多毫秒变成了1W多毫秒,这个break加的很OK。 回到我们刚才,平时需要for循环里面再for循环这种方式,可以看到耗时是2万6千多毫秒。 那如果场景更复杂一定,是for循环里面for循环多个或者,for循环里面还有一层for循环,那这样代码耗时真的非常恐怖。 那么接下来这个技巧点是使用map去优化: 代码:publicstaticvoidmain(String〔〕args){ListUseruserTestListgetUserTestList();ListUserMemouserMemoTestListgetUserMemoTestList();StopWatchstopWatchnewStopWatch();stopWatch。start();MapLong,StringcontentMapuserMemoTestList。stream()。collect(Collectors。toMap(UserMemo::getUserId,UserMemo::getContent));for(Useruser:userTestList){LonguserIduser。getUserId();StringcontentcontentMap。get(userId);if(StringUtils。hasLength(content)){System。out。println(模拟数据content业务处理。。。。。。content);}}stopWatch。stop();System。out。println(最终耗时stopWatch。getTotalTimeMillis());} 看看耗时: 为什么这么显著的效果? 这其实就是时间复杂度,for循环嵌套for循环,就好比循环每一个user,拿出userId需要在里面的循环从userMemolist集合里面按顺序去开盲盒匹配,拿出第一个,看看userId,拿出第二个,看看userId,一直找匹配的。 而我们提前对userMemolist集合做一次遍历,转存储在map里面。 map的取值效率在多数的情况下是能维持接近O(1)的,毕竟数据结构摆着,数组加链表。 相当于拿到userId想去开盲盒的时候,根据userId这个keyhash完能直接找到数组里面的索引标记位,如果底下没链表(有的话O(logN)),直接取出来就完事了。 然后补充一个getNode的代码注释:ImplementsMap。getandrelatedmethods。这是个Map。get的实现方法paramhashhashforkeyparamkeythekeyreturnthenode,ornullifnonefinal写死了无法更改返回Node传入查找的hash值和key键finalNodeK,VgetNode(inthash,Objectkey){tab还是哈希表first哈希表找的链表红黑树对应的头结点e代表当前节点k代表当前的keyNodeK,V〔〕tab;NodeK,Vfirst,e;intn;Kk;赋值并过滤哈希表空的长度不够的对应位置没存数据的都直接returnnullif((tabtable)!null(ntab。length)0(firsttab〔(n1)hash〕)!null){头结点就找到了hash相等值相等或者不空的key和当前节点equalsif(first。hashhashalwayscheckfirstnode((kfirst。key)key(key!nullkey。equals(k))))returnfirst;头结点不匹配没找到就就用next找if((efirst。next)!null){是不是红黑树的if(firstinstanceofTreeNode)return((TreeNodeK,V)first)。getTreeNode(hash,key);红黑树就直接调用红黑树内查找不为空或者没找到就dowhile循环do{当前节点找到了hash相等值相等或者不空的key和当前节点equalsif(e。hashhash((ke。key)key(key!nullkey。equals(k))))returne;}while((ee。next)!null);}}returnnull;} 按照目前以JDK8的hash算法,起hash冲突的情况是非常非常少见了。 最恶劣的情况,只有当全部key都冲突,全都分配到一个桶里面去都占用一个位置,这时候就是O(n),这种情景不需要去考虑。 原文链接;https:mp。weixin。qq。comsYok5B9j6nbK1o3fn6TA