前言
前两天下班和一朋友一起,被问到如下问题:如何移除一个数组中某些符合条件(或不符合条件)的元素形成新的数组,要求数组长度也应该变化(这是我后面加的)。
这两天研究下了这个问题,感觉比较有意思,也从中学到了一些其它该注意的东西。
特此来分享一下。
如有问题欢迎批评指正。
正文
对于上面的问题,最简单也最应该想到的一个应该是借助List这个工具类。
是的,我写下大致如下代码:
(1)使用List帮助类实现,借助remove方法
优点:代码简洁,便于理解,不易出错
缺点:数组和List转换耗时,效率不算太高,使用List remove方法时应注意线程安全问题,如果是基本数据类型,需要转换为包装类,拆箱装箱也是影响效率的一个因素。
代码如下:
1 | /** |
注: Predicate
关于这种写法,有需要注意的几点:
for-each,for, iterator 这三种对于List的循环,for,iterator是可以遍历并进行remove操作的,但是for-each是不可以的。有兴趣的可以研究研究,这不是我们的重点。
看到我上面使用的泛型T,其实使用Object[]也是可以的,要注意一个问题,泛型T是不能包含int,long等基本数据类型的,使用的话只能转化为它们的包装类。
Object[]是没有限制的,但是Object[]转换为Integer[]或者int[]或者其他不能直接转换,如下写法是错误的,会出现问题。
1
2Object [] objects=new Object[10];
Integer [] a=(Integer[])objects;正确的转换方法应该循环里面的元素,并对它们强转添加进数组;或者使用下面的方法。
1
2
3Object [] objects=new Object[10];
//Integer [] a=(Integer[])objects;
Integer [] a=(Integer[])Array.newInstance(Integer.class,objects.length);当我们把数组转换成List的时候,为了不想循环遍历添加,可能会想到使用Arrays.asList(T t) 这个方法,这个方法生成的List是一个Arrays里面的一个内部类ArrayList。
看下这个内部类你会发现它没有对remove、add等方法的实现,也就是继承自AbstractList。
1 | public E remove(int index) { |
也就是执行它们时会抛出异常,如果想使用remove方法,应该把它转为具体实现,如ArrayList。
1 | List<T> sourceList= new ArrayList<>(Arrays.asList(sourceArray)); |
为什么它要使用内部类处理这个方法呢?也是比较有趣的,我这儿还没做深入研究,有时间研究下。
(2)使用两次循环实现。
原理是第一次数组循环查找符合条件(或不符合条件)的个数count,后面在创建一个指定长度(原数组总长度-count)的数组,然后在遍历循环一遍原数组,将符合条件(或不符合条件)的元素添加进新的数组。
优点:简单直接,易于理解,基本数据类型数组的处理应该比List方法有优势。
缺点:两次循环应该比较耗时,对于长Array应该显现的明显。
代码如下:
1 | /** |
注意:还有一种情况,如果过滤的数据(符合条件或者不符合条件的)出现次数较低,我们是不是可以考虑一次拿出一整段进行处理。当然,如果频率较高,比如在一堆自然数中取偶数,明显奇数偶数出现频率相近,都为50%,那么我们可能用上面这种方法效率也很好。
(3)预先设置等长数组,而后截取得到目标数组
对比上一种方法,这是一种空间换时间的做法。
开始时创建一个和原数组相同大小的数组,遍历后把元素放进去,最后将数组截短。
这种方法仅仅循环一次。
代码如下:
1 | /** |
(4)借助List的toArray方法
借助List实现循环一次把符合条件的放到里面,再把List强转成数组。也是不错的实现方法。
代码如下:
1 | /** |
无论上面哪种方法,其底层都使用了System.arraycopy方法。
1 | /** |
(5)Java8串行处理方式
Java8中使用Stream操作集合工具类来对其进行处理。分为串行和并行两种方式。先来看看串行。
代码如下:
1 | /** |
其底层也是元素的循环遍历。
(6)Java8并行处理方式
我们应该知道,无论方法怎样,至少应该遍历数组一次以判断该元素是否符合条件。当数据量较大时,这儿会成为方法运行时间的瓶颈,由于List家族中ArrayList是有序的,我们可以使用多线程对它进行分割,每段进行遍历筛选结果,最后再把结果合起来。
并行流就是利用分支/合并框架实现的,使用了多线程。当数组数据量较大时效率是明显的。
Java8的相关API已经封装好,我们可以直接使用。
代码如下:
1 | /** |
(7)自己动手,丰衣足食
我们其实是可以借助多线程自己实现一个相似工具类的。
可以使用分支/合并框架自己实现一个多线程的处理。
关于这一块,我有一篇文章 一道Java试题引发的思考
中有具体例子及测试。
大家可以看下,数据量大的情况下并行效率还是比较明显的。
我这儿对这个例子就不在验证了。
注意:使用并行流(或者说多线程)要注意的点。
首先是数据量,数据量的主要意义就在于单线程处理的耗时(处理数据的时间)已经超过了多线程耗时(数据处理时间+拆分数据时间+合并结果时间),这一点是比较难把控的。其次一点是要确定这些数据可以使用多线程处理,不会产生意外的情况,比如我们这个问题,我想删除两个数相差1的所有元素,剩下的元素生成一个新的数组,多线程显然不易解决这种问题,或者解决起来较复杂。
测试
测试的话今天就省了,(3)、(4)、(5)都是不错的写法,(6)话具体问题具体分析,(7)的话有想法的可以试试,(1),(2)不推荐。
因为数据量大小,数据类型都对方法有些影响。
比如较短的原数组,基本数据类型,(3)方法效率很快的,对比(4)、(5)是没有数据拆箱操作的。换成长数组,引用数据类型,(6)可能效率就高了。
结语
开始写这篇文章的时候脑子不好使,根本没想到Java8的Stream,失误。我甚至一开始想的都是些可能不安全((1)方法),或者比较繁琐((2)方法)的方法,过了一天晾了晾脑子就好使多了。
在处理时,因为数组底层操作都是基于System.arraycopy嘛,我想到是不是循环一次记录符合条件(不符合条件)的元素下标(可用List记录),然后建立一个目标数组,使用System.arraycopy一段一段的将数据copy进去。
无奈才疏学浅,想了半天使用System.arraycopy时的两个起始位置,copy长度始终弄错了,仔细想了下,估计这种方法效率也不怎么高。
哈哈,于是就没写。
大家有什么好的、耳目一新的方法也可以说出来交流交流。