南山商城网站建设,乐清市规划图高清,wordpress短代码可视化,网站设计服务平台作者简介#xff1a;大家好#xff0c;我是smart哥#xff0c;前中兴通讯、美团架构师#xff0c;现某互联网公司CTO 联系qq#xff1a;184480602#xff0c;加我进群#xff0c;大家一起学习#xff0c;一起进步#xff0c;一起对抗互联网寒冬 学习必须往深处挖… 作者简介大家好我是smart哥前中兴通讯、美团架构师现某互联网公司CTO 联系qq184480602加我进群大家一起学习一起进步一起对抗互联网寒冬 学习必须往深处挖挖的越深基础越扎实
阶段1、深入多线程阶段2、深入多线程设计模式阶段3、深入juc源码解析阶段4、深入jdk其余源码解析阶段5、深入jvm源码解析
尽管在之前介绍了如何避免并发修改异常但那篇文章的目的更多的是为了介绍底层原理及应付面试实际开发中并不推荐大家对原List做增删改操作。
我的观点是对于一个初始化完毕的List尽量把它当做只读的不要贸然做增删改操作。比如Java8的Stream它所有的操作都是基于新的List并不会改变原数据包括JDK、Google Common以及Apache Common等工具类提供的不可变集合Immutable Collections其实都是在传递这种思想Google Common甚至直接屏蔽了增删改方法 接下来给大家分享两个实际开发中遇到的问题都与List操作有关。 用skip()、limit()代替subList()
对于List的截取可能大家都习惯用List.subList()但它有个隐形的坑对截取后的List进行元素修改会影响原List除非你就希望改变原List。
究其原因subList()并非真的从原List截取出元素而是偏移原List的访问坐标罢了 比如你要截取(5, 6)那么下次你get(index)我就直接返回5index给你看起来好像真的截取了。 另外这个方法限制太大用起来也麻烦比如对于一个不确定长度的原List如果你想做以下截取操作list.subList(0, 5)或者list.subList(2, 5)当原List长度不满足List.size()5时会抛异常。为了避免误操作你必须先判断size
if(list ! null list.size() 5) {return list.subList(2, 5);
}
较为简便和安全的做法是借助StreamStream一个很重要的特性是不修改原数据而是新产生一个流
public static void main(String[] args) {ListString list Lists.newArrayList(a, b, c, d);ListString limit3 list.stream().limit(3).collect(Collectors.toList());// 超出实际长度也不会报错ListString limit5 list.stream().limit(5).collect(Collectors.toList());ListString range3_4 list.stream().skip(2).limit(2).collect(Collectors.toList());// 超出实际长度也不会报错ListString range3_5 list.stream().skip(2).limit(3).collect(Collectors.toList());System.out.println(limit3 limit5 range3_4 range3_5);
} 用filter()代替remove()
很多同学对内存占用极其敏感恨不得用同一份内存把A、B、C三件事都干了特别是经历了LeetCode摧残的人。这种想法是好的但对于List这样有并发修改限制的容器来说一不留神就有可能出现问题。举个例子 假设后台要支持配置定向推广的商品并且需要将配置的商品在当前时间轴置顶比如09:00下。原本时间轴的列表是AList长度为10而后台配置的商品为BList长度不确定在0~10之间。考虑到后台配置的商品可能与原List中的商品重复所以这里要加一个去重操作。很多人可能会想到利用Set或者Map的key不重复的特性去重但试了以后会发现顺序可能被打乱。那么最直观的方法就是双层for遍历先遍历原来的AList然后拿着AList的item去BList遍历如果这个item在BList中已经存在就把这个item从AList删除。
public class ListRemoveTest {public static void main(String[] args) {// 前台ListListItem aList Lists.newArrayList(new Item(1, 甲),new Item(2, 乙),new Item(3, 丙));// 后台ListListItem bList Lists.newArrayList(new Item(99, 对照数据),new Item(3, 丙));// 对前台List去重for (int i aList.size() - 1; i 0; i--) {for (Item user : bList) {if (Objects.equals(user.getId(), aList.get(i).getId())) {aList.remove(i);}}}// 组合去重后的两个List后台List置顶bList.addAll(aList);System.out.println(JSON.toJSONString(bList));}GetterSetterAllArgsConstructorstatic class Item {private Integer id;private String title;}}
即使我对并发修改异常“了如指掌”在实际开发时还是写出了上面的代码。最致命的是上面的代码还不一定会出错如果重复商品只有一个且恰好出现在bList的末尾上面的代码是不会报错的。如果我们将上面bList元素顺序对调再次运行就会发生数组越界异常 原因是当bList重复的元素只有一个且恰好在末尾时第二层for在执行aList.remove()以后就直接退出第二层for不会继续执行if逻辑也就不会执行aList.get(i)所以不会发生数组越界可能比较难理解大家可以复制代码实际观察一下。 当初虽然考虑到并发修改异常的可能但不巧的是构造测试数据时只构造了一个重复的商品而且排序系数设置为最高恰好处于bList的末尾完美地避开了问题...实际上线几天后的某个早晨运营配置了多个商品而且恰好重复了于是首页直接崩了...这是一个很严重的事故。 一个可行的处理方式是
public static void main(String[] args) {// 前台ListListItem aList Lists.newArrayList(new Item(1, 甲),new Item(2, 乙),new Item(3, 丙));// 后台ListListItem bList Lists.newArrayList(new Item(3, 丙),new Item(99, 对照数据));// 对aList进行筛选bList中不存在的itemMapInteger, Item bItemMap bList.stream().collect(Collectors.toMap(Item::getId, v - v, (v1, v2) - v1));ListItem filteredAList aList.stream().filter(aItem - !bItemMap.containsKey(aItem.getId())).collect(Collectors.toList());// 组合去重后的两个List后台List置顶bList.addAll(filteredAList);System.out.println(JSON.toJSONString(bList));
}
当然List本身提供了诸如allAll()、retainAll()、removeAll()等操作可以很方便的实现并集、交集、差集。所以上面的去重取并集可以这样
public class ListRemoveTest {public static void main(String[] args) {// 前台ListListItem aList Lists.newArrayList(new Item(1, 甲),new Item(2, 乙),new Item(3, 丙));// 后台ListListItem bList Lists.newArrayList(new Item(3, 丙),new Item(99, 对照数据));// 先去重再合并aList.removeAll(bList);bList.addAll(aList);System.out.println(JSON.toJSONString(bList));}GetterSetterAllArgsConstructorEqualsAndHashCode // 注意这里要重写equals和hash否则默认比较地址值static class Item {private Integer id;private String title;}}
说了这么多就是想强调无论是并发修改异常还是数组越界通常情况下都不会发生但当你企图对原List进行增删改操作时只要没考虑周全就有极大概率发生。由于Stream的任何操作都不会改变原数据所以从根源上杜绝了增删改可能隐藏的问题是比较安全的方式也推荐大家多使用Stream无论从代码可读性还是健壮性来说都会好很多。