Java中List遍历的几个问题

2020-12-03 18:04:23 浏览数 (1)

参考链接: Java中的循环的重要事项

之前在项目中引入Lambda表达式后,最近就把之前的代码改为Lambda表达式,中间遇到了一个小插曲就是List的在调用Stream的forEach()中使用return 、break、continue关键字的问题;加上最近一直关注的“码农每一题”于是自己回顾一下List的基础温故而知新了; 

一、List几种遍历方式的问题 

 Java 中常见的几种遍历方式方式:1.loop without size / for(i=0;i<expr.length-1;i )2.foreach/ for(T item:expr)3.Iterator/迭代器4.Stream.forEach()5.parallelStream().forEach(); 

问题1:foreach增强for循环中修改List中element的值操作无效; 

示例代码: 

 public static void main(String[] args) {

        int size = 1000;

        String s[] = new String[]{"qwqwe", "frsgdf", "asd", "dfsfuytrd", "qwds"};

        List<String> asList = Arrays.asList(s);

        for (String t : asList) {

            if (t.length() <= 4) {

                System.out.println(t);

                t = "1122";

            }

        }

        for (String tt : asList){

            System.out.println("==== :" tt);

        }

}        

//程序运行结果

asd

qwds

==== :qwqwe

==== :frsgdf

==== :asd

==== :dfsfuytrd

==== :qwds

问题缘由: 

foreach遍历JDK5.0增加的增强for循环,foreach在遍历过程中是通过一个临时变量,记录遍历到的当前List中的element,所以在 foreach中操作的对象是指向临时变量的,而不是List中的element实例对象的地址,结果自然就只是修改临时变量的值并没修改List中的element,所以才会出现:foreach增强for循环中修改List中element的值是无效的问题; 

  解决办法: 改用loop without size实行; 

问题2:Iterator迭代时,调用List集合对象remove(Object o)时,抛出Exception; 

示例代码: 

 public static void main(String[] args) {

        List<String> asList = new ArrayList<>();

        asList.add("qwqwe");

        asList.add("frsgdf");

        asList.add("asd");

        asList.add("dfsfuytrd");

        asList.add("qwds");

        Iterator<String> iterator = asList.iterator();

        String next;

        while (iterator.hasNext()) {

             next = iterator.next();

            if(next.length()<=4){

                asList.remove(next);

            }

        }

}

//运行结果

Exception in thread "main" java.util.ConcurrentModificationException

    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)

    at java.util.ArrayList$Itr.next(ArrayList.java:851)

    at Main.main(Main.java:31)

问题缘由: 

这个问题是和Iterator的实现方式有关系的,以ArrayList为例,在ArrayList中.iterator()其实是通过工厂模式在内部new出来一个Iterator对象,而且这个Iterator对象的size是按照它被创建时List的.size()大小创建的,如果在iterator()中调用List的remove方法,这就会导致Iterator的size大于List的size,进而发生IndexOutOfBoundsException越界异常(List中改为抛出ConcurrentModificationException,可参考ArrayList.Itr.next()函数); 

  解决办法: 1.如果list中需要删除一个element的操作的话可以的话,删除完成直接break;这样也可以节约时间和减小性能开销;2.调用Iterator的remove()方法进行删除【在源码中可以看到在Iterator的remove()中同时也调用了List的remove(),这保持了List的size和Iterator的size一致,避免出现越界异常;】 

问题3:JDK8中Stream.forEach()遍历时return、break、continue关键字使用【parallelStream也存在这样问题】; 

在JDK8中引入的Stream中利用forEach()遍历List中,发现break和continue两个关键字IDE会直接提示语法错误的,所以这连个关键字就直接可以pass了,直接看return吧; 示例代码: 

 public static void main(String[] args) {

  List<String> list = Arrays.asList("qwqwe", "frsgdf", "asd", "dfsfuytrd", "qwds");

        list.stream().forEach(e -> {

            if (e.length() <= 4) {

                System.out.println("----" e);

                return;

            }

            System.out.println(e);

        });

 }

 //程序执行结果:

 qwqwe

frsgdf

----asd

dfsfuytrd

----qwds

问题缘由: 

在stream[parallelStream中也是一样的]中关键字return、break、continue关键字使用问题是和Java8中流Stream的设计有关系的,在Java8中引入的流的目的是提高并发执行效率即:Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation); 可以理解为Stream操作的对象是Collection)集合对象自身而不是集合Collection中的element,而且Stream中的各个方法都是一旦开始执行就没有回头路,只能等待全部执行完成。 

1.break和continue关键字在Sream中失效问题,个人理解为:Stream每次执行的是对整个集合为最小操作单元,而break和continue是以集合Collection中的element为操作单元的,所以这两关键字在设计上就不是一个量级的,所以它们在Stream面前就失效了;2 .return 在遍历结果来看其实充当了continue的角色,同样return在整个Java中的方法中充当了“急刹车和掉头返回”的功能,根据上面的理解:在上述代码中return的对象只是Steam操作中的一个小支流element,所以return也只能对其中目标element进行刹车,并不能阻止其他element的继续,结果就是return在Stream中充当continue成了一个既成事实; 

  解决办法:  个人的观点是先搞清楚基本原理,根据实际需求灵活选择;  小插曲  刚开始看结果给人的感觉就是return充当了continue的角色,而且还是按照List的顺序执行的,菜鸡还是百度了一下结果都说Java8中的stream是并发的数据量大的话就可能是出现乱序,于是赶紧自己测试了1000个String结果任然是按顺序打印的,又在CSDN中看到有人说String太简单,于是new了一个JavaBean结果还是按照顺序打印,于是越发感觉网上"那些人是胡说",再者在源码看到stream和parallelStream二者差别时,更加确认stream是sequential有序的,而parallelStream才是parallel无序的;  

二、List几种遍历方式的效率问题 

Java一直被人诟病的就是效率问题了,所以最后咋能不简单的对比一下呢; 

  基础测试前准备问题 1.经验告诉我们是性能越差劲的设备[CPU、RAM]越能放大代码效率差异,所以选择在Android手机上进行测试;2.测试选择ArrayList和LinkedList两种最长用到的List,同时也是两种不同数据结构的List进行验证;3.测试Size分别选择size为50,500,1000,5000,10000,50000作为验证变量;4.测试List遍历的对象为JavaBean【有String.int long三种基本类型,且每次遍历都是相同打印操作】;5.测试过程中所有的遍历方式中操作完全相同;6.测试过程中每次测试前杀死手机其他app,完成一次测试后杀死测试app等一小会尽可能消除内存影响; 

测试结果为: 

基本结论: 

1.随着Size的逐渐变大parallelStream遍历的效率就越明显,在Size达到5000 以后parallelStream遍历时间基本上是其他遍历方式的时间的一半 ;2.根据测试结果,在JDK8之前几种遍历的方式中通过Size循环遍历效率最差,Iterator和foreach效率基本差不多,但是foreach代码更简洁;3.在parallelStream遍历中LinkedList的遍历效率明显优于ArrayList;这是和LinkedList的数据结构以及parallelStream的遍历逻辑有关系的4.JDK8中引入是stream在List的size在5000以下时遍历的时间由于其他遍历方式【parallelStream以外】这个结果不知道正确不; 

测试的几个问题: 

1.在测试过程中发现同样的Size测试几次结果几乎每次都有细微的差异,个人分析认为是和测试时手机状态有关系,不同时间手机系统内部不同操作导致CPU占用情况不相同导致的;2.这个测试数据结果中并没有很明显体现出ArrayList和LinkedList相比在查询的中的优势:在foreach遍历方式中二者时间基本上没有差异;这个有点不太明白是什么原因导致的,希望有哪位大佬答疑解惑。 

【下文中已经指出问题根源和改进建议】 

三、重要的补充 

对这是一个重要的补充,是针对的上文中对List测试的一个重要补充。 在最近准备看面试题看到关于try catch性能影响时,看到的一篇博文try catch 对性能影响中不正确测试后,于是赶紧写代码测试[重现之前测试会自相矛盾的结果]验证之前测试方式的错误;于是解开了笔者在上文中测试结果困惑;由于本人是半路的码农,以目前个人的认知水平经过测试验证后,个人十分认同try catch 对性能影响提出的问题和解决思路。 下文是在大量引用try catch 对性能影响文中的内容同时夹杂少部分个人理解,如有错误纰漏望诸位及时指正; 

1.测试中的问题 

a、System.currentTimeMillis()和System.nanoTime()测测量程序运行耗时的不准确 

首先System.currentTimeMillis()和System.nanoTime()得到的只是当前时间点,前后时间差只是两次调用代码的时间差;这中间不仅仅只有函数的运行时间还有线程抢占CPU资源时的等待时间,所以难以保证时间的准确性; 

b、Java中JIT优化导致结果出现偏差; 

在JVM中的JIT的JIT优化同样会导致结果出现偏差; 

  JIT: 在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。  热点代码(Hot Spot Code): 当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。 在某些情况下,调整好的最佳化 JVM 效能可能超过手工的 C 或 C;  运行过程中会被即时编译器编译的“Hot Spot Code”有两类: 多次调用的方法、多次调用的循环体。  

显然测试代码正是典型的:频繁的循环的循环体,JIT也增添了更大误差; 

c、类加载时间和程序运行时间叠加 

在首次run的时候类的加载时带来的时间误差; 

2.正确的测试方式 

a、不要使用System.currentTimeMillis()亦或者使用System.nanoTime() ; 

这里说明一下,可能你会看到有些建议使用System.nanoTime()来测试,但是它跟System.currentTimeMillis()区别,仅仅在于时间的基准不同和精度不同,但都表示的是逝去的时间,所以对于测试执行时间上,并没有什么区别。因为都无法统计CPU真正执行时间。 

b、推荐使用JProfiler性能测试工具 

要测试cpu真正执行时间,这里推荐使用JProfiler性能测试工具,它可以测量出cpu真正的执行时间。具体安装使用方法可以自行google百度。因为这不是本文最终使用的测试方法,所以就不做详细介绍了。但是你使用它来测试上面的代码,至少可以排除等待CPU消耗的时间 对于后两者,需要加入Warmup(预热)阶段。 预热阶段就是不断运行你的测试代码,从而使得代码完成初始化工作(类加载),并足以触发JIT编译机制。一般来说,循环几万次就可以预热完毕。 

那是不是做到以上两点就可以了直抵真相了?非常不幸,并没有那么简单,JIT机制和JVM并没有想象的这么简单,要做到以下这些点你才能得到比较真实的结果。建议参考how-do-i-write-a-correct-micro-benchmark-in-java排名第一的答案; 还可以参考Java theory and practice: Anatomy of a flawed microbenchmark 认真看完这些,你就会发现,要保证microbenchmark结果的可靠,真不是一般的难!!! 

那就没有简单可靠的测试方法了吗?如果你认真看完上面提到的点,你应该会注意到Rule 8,没错,我就是使用Rule8提到的JMH来。 JMH官方主页:http://openjdk.java.net/projects/code-tools/jmh/ 

四、附上错误的测试代码和手机参数 

测试Android手机:华为畅享7(SLA-AL00/2GB RAM/全网通); 测试完整代码: 

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.button_a)

    Button buttonA;

    private static int size = 1000;

    @BindView(R.id.size)

    EditText dt_size;

    @BindView(R.id.button_b)

    Button buttonB;

    @BindView(R.id.textView)

    TextView textView;

    private ArrayList<TestBean> as;

    private LinkedList<TestBean> ls;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        ButterKnife.bind(this);

    }

    @SuppressLint("NewApi")

    @OnClick({R.id.button_a, R.id.button_b})

    public void onViewClicked(View view) {

        switch (view.getId()) {

            case R.id.button_a:

                initData();

                break;

            case R.id.button_b:

                test();

                break;

        }

    }

    private void initData() {

        size = Integer.parseInt(dt_size.getText().toString());

        Log.e("TAG", "initData: ======= > SIZE :" size);

        as = null;

        ls = null;

        as = new ArrayList<>(size);

        ls = new LinkedList<TestBean>();

        for (int i = 0; i < size; i ) {

            this.as.add(new TestBean("AS index :" i, System.currentTimeMillis(), i));

            this.ls.add(new TestBean("LS index :" i, System.currentTimeMillis(), i));

        }

        Log.e("TAG", "initData: ======= > init date over");

    }

    @SuppressLint("NewApi")

    private void test() {

        StringBuilder sb = new StringBuilder();

        /*------------------------------loop without size------------------------------*/

        long l1 = System.currentTimeMillis();

        for (int i = 0; i < size; i ) {

            Log.i("TAG", "AS: ---- > " as.get(i).toString());

        }

        long l = System.currentTimeMillis() - l1;

        sb.append("AS loop without :" l).append(" _ ");

        long l2 = System.currentTimeMillis();

        for (int i = 0; i < size; i ) {

            Log.i("TAG", "LS: ---- > " ls.get(i).toString());

        }

        long ll = (System.currentTimeMillis() - l2);

        sb.append("LS loop without :" ll).append(" _ ");

        /*-----------------------------------foreach-------------------------*/

        long l3 = System.currentTimeMillis();

        for (TestBean bean : as) {

            Log.i("TAG", "AS: ---- > " bean.toString());

        }

        long l_3 = (System.currentTimeMillis() - l3);

        sb.append("AS foreach  :" l_3).append(" _ ");

        long l4 = System.currentTimeMillis();

        for (TestBean bean : ls) {

            Log.i("TAG", "LS: ---- > " bean.toString());

        }

        long l_4 = (System.currentTimeMillis() - l4);

        sb.append("LS foreach  :" l_4).append(" _ ");

        /*-------------------------------Iterator-----------------------------*/

        long l5 = System.currentTimeMillis();

        Iterator<TestBean> iterator = as.iterator();

        while (iterator.hasNext()) {

            Log.i("TAG", "AS: ---- > " iterator.next().toString());

        }

        long l_5 = (System.currentTimeMillis() - l5);

        sb.append("AS Iterator  :" l_5).append(" _ ");

        long l6 = System.currentTimeMillis();

       Iterator<TestBean> Literators = ls.iterator();

      while (Literators.hasNext()) {

            Log.i("TAG", "LS: ---- > " Literators.next().toString());

        }

        long l_6 = (System.currentTimeMillis() - l6);

        sb.append("LS Iterator  :" l_6).append(" _ ");

        /*----------------------------------stream--------------------------*/

        long l7 = System.currentTimeMillis();

        as.stream().forEach(e -> Log.i("TAG", "AS: ---- > " e.toString()));

        long l_7 = (System.currentTimeMillis() - l7);

        sb.append("AS stream  :" l_7).append(" _ ");

        long l8 = System.currentTimeMillis();

        ls.stream().forEach(e -> Log.i("TAG", "LS: ---- > " e.toString()));

        long l_8 = (System.currentTimeMillis() - l8);

        sb.append("LS stream  :" l_8).append(" _ ");

        /*---------------------------------parallelStream---------------------------*/

        long l9 = System.currentTimeMillis();

        as.parallelStream().forEach(e -> Log.i("TAG", "AS: ---- > " e.toString()));

        long l_9 = (System.currentTimeMillis() - l9);

        sb.append("AS parallelStream  :" l_9).append(" _ ");

        long l10 = System.currentTimeMillis();

        ls.parallelStream().forEach(e -> Log.i("TAG", "LS: ---- > " e.toString()));

        long l_10 = (System.currentTimeMillis() - l10);

        sb.append("LS parallelStream  :" l_10).append(" _ ");

        textView.setText(sb.toString());

        Log.e("TAG", "-------------------------------TEST OVER----------------------------------");

        Log.e("TAG", "---------------- RESULT : " sb.toString());

    }

    class TestBean {

        private String str;

        private long time;

        private int index;

        public TestBean(String str, long time, int index) {

            this.str = str;

            this.time = time;

            this.index = index;

        }

        @Override

        public String toString() {

            return "TestBean{" "str='" str ''' ", time=" time ", index=" index '}';

        }

    }

}

测试主页View

0 人点赞