Knowledge Point 20180308 拔下forEach的外衣
剖析加强for
很长一段时间对于foreach都有一种误解,那就是foreach只是普通for的包装,底层还是普通for循环,直到深入了解迭代器的时候,才发现自己错了,本节就来探讨一下foreach,深入底层去了解它。下面我们通过一段代码来看一下:
public static void listIterator(){
List<String> list = new ArrayList<>();
list.add("野有蔓草,零露漙兮。");
list.add("有美一人,清扬婉兮。");
list.add("邂逅相遇,适我愿兮。");
list.add("野有蔓草,零露瀼瀼。");
list.add("有美一人,婉如清扬。");
list.add("邂逅相遇,与子偕臧。");
for (String string : list) {
System.out.println(string);
}
}

想要了解它的底层实现,自然需要它编译后的代码,下面我们通过反编译查看一下:

这样看起来可能不好理解,我找了一款反编译软件,查看反编译后的结果:
public static void listIterator()
{
List list = new ArrayList();
list.add("\u91CE\u6709\u8513\u8349\uFF0C\u96F6\u9732\u6F19\u516E\u3002");
list.add("\u6709\u7F8E\u4E00\u4EBA\uFF0C\u6E05\u626C\u5A49\u516E\u3002");
list.add("\u9082\u9005\u76F8\u9047\uFF0C\u9002\u6211\u613F\u516E\u3002");
list.add("\u91CE\u6709\u8513\u8349\uFF0C\u96F6\u9732\u703C\u703C\u3002");
list.add("\u6709\u7F8E\u4E00\u4EBA\uFF0C\u5A49\u5982\u6E05\u626C\u3002");
list.add("\u9082\u9005\u76F8\u9047\uFF0C\u4E0E\u5B50\u5055\u81E7\u3002");
String string;
for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(string))
string = (String)iterator.next();
}
上面的代码我们知道了两个问题,Java真的是通过Unicode编码的,不过这不是重点,重点是遍历,我们发现编译器将加强for循环,编程了普通for循环,通过迭代器进行循环操作。
在编译的时候编译器会自动将对for这个关键字的使用转化为对目标的迭代器的使用,这就是foreach循环的原理。进而,我们再得出两个结论:
- ArrayList之所以能使用foreach循环遍历,是因为ArrayList所有的List都是Collection的子接口,而Collection是Iterable的子接口,ArrayList的父类AbstractList正确地实现了Iterable接口的iterator方法。注意:如果自己写的ArrayList用foreach循环直接报空指针异常是因为自己写的ArrayList并没有实现Iterable接口
- 任何一个集合,无论是JDK提供的还是自己写的,只要想使用foreach循环遍历,就必须正确地实现Iterable接口;
实际上,这种做法就是23中设计模式中的迭代器模式。查看了加强for对集合的操作,我们思考一个问题,如果加强for是对实现Iterable接口的一种优化,那么数组呢?Arrays并没有实现该接口,但是加强for仍然能够应用于数组,下面我们再来看一下数组的加强for遍历:
public static void arrFor(){
int[] arr = {1,2,3,4,5};
for (int i : arr) {
System.out.println(i);
}
}
一样通过反编译查看:
public static void arrFor()
{
int arr[] = {
1, 2, 3, 4, 5
};
int ai[];
int k = (ai = arr).length;
for(int j = 0; j < k; j++)
{
int i = ai[j];
System.out.println(i);
} }
观察发现Java将对于数组的foreach循环转换为对于这个数组每一个的循环引用,且操作的是数组的一个备份,而非是数组本身,所以在加强for中对于数组的操作,不会影响到数组本身,通过下面的例子观察一下:
public static void arrFor(){
int[] arr = {1,2,3,4,5};
for (int i : arr) {
i = 9;
System.out.println(i);
}
System.out.println(Arrays.toString(arr));
}

但是对于集合却和数组不同了,集合可以直接操作到本身,我们还是通过例子查看:
public static void listIterator(){
List<String> list = new ArrayList<>();
list.add("野有蔓草,零露漙兮。");
list.add("有美一人,清扬婉兮。");
list.add("邂逅相遇,适我愿兮。");
list.add("野有蔓草,零露瀼瀼。");
list.add("有美一人,婉如清扬。");
list.add("邂逅相遇,与子偕臧。");
for (String string : list) {
// string = "改变";
if(string.equals("邂逅相遇,适我愿兮。")){
list.remove(string);
}
System.out.println(string);
}
System.out.println(list.toString());
}

我们发现在加强for中对于集合的改变是不被允许的,将注释去掉,同时注释掉删除语句,查看结果如下:

我们发现仍然对于集合没有改变,使用JDK1.5提供的foreach循环来迭代访问集合元素更加便捷。当使用foreach循环迭代访问集合元素时,该集合也不能被改变。
第一种remove时的异常是因为,更改了集合长度,此时后台方法(ArrayList)中判断抛出异常

而在加强for中改变元素的内容,断点调试发现,改变只是将字符串的索引改变了,但是集合中字符串指向并没有改变;

总结:
通过对于foreach的剖析,我们知道,加强for会在编译器编译后回归到普通for循环,加强for语法内部对于其操作有两种实现(数组和集合),加强for是一个语法糖,对于简便开发具有帮助,但是并不能用于性能提升。
- For-each语法内部,对collection是用iterator来实现的,对数组用下标遍历来实现。所以在对数组以外进行遍历的时候,要求实现接口Iterable。
- 加强for是一个语法糖,它帮助程序员简便开发,但是对于性能却并没有帮助,这个需要注意。Java 5 及以上的编译器隐藏了基于iterator和下标遍历的内部实现。(注意,这里说的是“Java编译器”或Java语言对其实现做了隐藏,而不是某段Java代码对其实现做了隐藏,也就是说,我们在任何一段JDK的Java代码中都找不到这里被隐藏的实现。这里的实现,隐藏在了Java 编译器中,我们可能只能像本文中说的那样,查看一段For-each的Java代码编译成的字节码,从中揣测它到底是怎么实现的了)
- 加强for循环只是JDK1.5提供的用于便于遍历的操作,并不能用来更改数组或集合,特别是更改集合可能会抛出异常,在这方面要留心注意;对于数组的操作是通过副本,对于集合如果想要在遍历时改变集合,则要使用迭代器,关于迭代器是集合中要讲解的,这里不做赘述。
本文只简单探讨加强for的底层实现,而不对迭代器进行探讨,迭代器放在集合中讲解。
Knowledge Point 20180308 拔下forEach的外衣的更多相关文章
- win7插着网线开机卡死,拔下网线开机正常
公司的部分win7电脑插着网线开机,进到桌面后网络图标转圈圈卡住.控制面板,启动项,任务管理器等都打不开.把网线拔下后再开机,电脑正常进入系统,后再插上网线就能正常上网了.被这个问题困扰了很久,百度也 ...
- U盘启动装完系统后 一拔下优盘 就不能进入系统
PE下Ghost安装的,装好进入系统正常,可是拔下 u盘就进不去系统,而插上 u盘就好好的 原因:引导的事,找到本地硬盘第一分区并且激活! 就可以了! 可用下“电脑店-修复主引导记录(MBR)工具”h ...
- MyBatis_动态sql_foreach_mysql下foreach批量插入的两种方式
方法1: 笔记要点出错分析与总结工程组织数据库组织0.重新修改Bean类 修改1.定义接口 //批量插入 public void addEmps(@Param("emps") ...
- Knowledge Point 20180308 Dead Code
不知道有没有前辈注意过,当你编写一段“废话式的代码时”会给出一个Dead Code警告,点击警告,那么你所写的废物代码会被编译器消除,那么如果你不理睬这个警告呢?编译后会是什么样的呢?下面我们写点代码 ...
- win10拔下电源会黑一下屏
- Ubuntu 64位下搭建ADT的种种问题
我使用的adt版本为 adt-bundle-linux-x86_64-20140702.zip 1. Eclipse启动时提示 adb 无法加载动态链接库 libstdc++.so.6 以及 lib ...
- Notes 20180308 : 语句
在讲解流程控制语句之前,我们先来说一下语句的问题.Java中的语句分为声明和赋值语句,条件和循环语句,调用和返回语句:我们之所以每两个放在一起是有深意的,我们大致将语句分为这三块,并以此为纲来说一下, ...
- foreach
一 foreach的语法介绍 PHP 4以上的版本包括了 foreach 结构,这只是一种遍历数组简便方法.foreach 仅能用于数组,当试图将其用于其它数据类型或者一个未初始化的变量时会产生 ...
- [译]何时使用 Parallel.ForEach,何时使用 PLINQ
原作者: Pamela Vagata, Parallel Computing Platform Group, Microsoft Corporation 原文pdf:http://download.c ...
随机推荐
- js 密码 正则表达式
1. 代码 function checkPassword(str){ var reg1 = /[!@#$%^&*()_?<>{}]{1}/; var reg2 = /([a-zA- ...
- Git 学习记录
安装 1. https://git-for-windows.github.io下载(网速慢的同学请移步国内镜像),然后按默认选项安装即可.安装完成后,在开始菜单里找到“Git”->“Git B ...
- Windows API编程----枚举系统进程
1.该函数可以检索系统中的每个进程的标识符(进程ID) BOOL WINAPI EnumProcesses( _Out_ DWORD *pProcessIds, _In_ DWORD cb, _Ou ...
- Javascript之DOM的三大节点及部分用法
DOM有三种节点:元素节点.属性节点.文本节点. 一.用nodeType可以检测节点的类型 节点类型 nodeType属性值 元素节点 1 属性节点 2 文本节点 3 这样方便在js中对各个节点进行操 ...
- 4.Linux文件系统层次体系标准
这是不完整的linux文件系统层次体系标准,不是所有Linux发行版都根据这个标准,但大多数都是: 目录 评论 / 根目录,万物起源. /bin 包含系统启动和运行所必须的二进制程序. /boot 包 ...
- SQL Server ->> 内置标量函数TRY_PARSE、TRY_CAST和TRY_CONVERT的各自特点和区别
SQL Server到了目前的2014版本有三个函数是用来转换数据格式的.虽说之前版本中已经有CAST和CONVERT这两个函数来干这个事情.问题是,一旦往目标数据类型转换失败就会造成报错. TRY_ ...
- JBOSS参数调优
阅读目录 JBOSS参数调优 jvm调优讲解1 JVM调优讲解2 JVM常见配置汇总 JBOSS生产环境下JVM调优 JBOSS瘦身 JBoss性能优化:内存紧张的问题终于解决了(转载)--- ...
- sql server——子查询
简述: 在查询语句中包含着有另外一条查询语句,被包含的查询语句称为子查询,包含着子查询的查询就称为父查询. 总之,子查询就是在查询语句里嵌套一条或者多条查询语句. 常用子查询分类: 一.独立子查询 特 ...
- stream操作 z
常见并常用的stream一共有 文件流(FileStream), 内存流(MemoryStream), 压缩流(GZipStream), 加密流(CrypToStream), 网络流(NetworkS ...
- Python 编码为什么那么蛋疼?
据说,每个做 Python 开发的都被字符编码的问题搞晕过,最常见的错误就是 UnicodeEncodeError.UnicodeDecodeError,你好像知道怎么解决,遗憾的是,错误又出现在其它 ...