面试官又整新活,居然问我for循环用i++和++i哪个效率高?

简介: 面试官又整新活,居然问我for循环用i++和++i哪个效率高?

前几天,一个小伙伴告诉我,他在面试的时候被面试官问了这么一个问题:

image.png

听到这,我感觉这面试官确实有点不按套路出牌了,放着好好的八股文不问,净整些幺蛾子的东西。在临走的时候,小伙伴问面试官这道题的答案是什么,面试官没有明确告诉答案,只是说让从程序执行的效率角度自己思考一下。

好吧,既然这个问题被抛了出来,那我们就见招拆招,也给以后面试的小伙伴们排一下坑。

image.png

前面提到,这个搞事情的面试官说要从执行效率的角度思考,那我们就抛开语义上的区别,从运行结果以外的效率来找找线索。回想一下,我们在以前介绍CAS的文章中提到过,后置自增i++和前置自增++i都不是原子操作,那么实际在执行过程中是什么样的呢?下面,我们从字节码指令的角度,从底层进行一波分析。

i++ 执行过程

先写一段简单的代码,核心功能就只有赋值和自增操作:

public static void main(String[] args) {
    int i=3;
    int j=i++;
    System.out.println(j);
}

下面用javap对字节码文件进行反编译,看一下实际执行的字节码指令:

image.png

是不是有点难懂?没关系,接下来我们用图解的形式来直观地看看具体执行的过程,也帮大家解释一下晦涩的字节码指令是如何操作栈帧中的数据结构的,为了简洁起见,在图中只列出栈帧中比较重要的操作数栈和局部变量表。

上面的代码中除去打印语句,整体可以拆分成两步,我们先看第一步 int i=3 是如何执行的 。

image.png

上面两条操作数栈和局部变量表相关的字节码指令还是比较容易理解的,下面再看一下第二步int j=i++的执行过程:

image.png

在上图中需要注意的是,iinc能够直接更新局部变量表中的变量值,它不需要把数值压到操作数栈中就能够直接进行操作。在上面的过程中,抛去赋值等其他操作,i++实际执行的字节码指令是:

2: iload_1
3: iinc    1, 1

如果把它翻译成我们能看懂的java代码,可以理解为:

int temp=i;
i=i+1;

也就是说在这个过程中,除了必须的自增操作以外,又引入了一个新的局部变量,接下来我们再看看++i的执行过程。

++i 执行过程

我们对上面的代码做一点小小的改动,仅把i++换成++i,再来分析一下++i的执行过程是怎样的。

public static void main(String[] args) {
    int i=3;
    int j=++i;
    System.out.println(j);
}

同样,用javap反编译字节码文件:

image.png

int i=3对应前两行字节码指令,执行过程和前面i++例子中完全相同,可以忽略不计,重点还是通过图解的方式看一下int j=++i对应的字节码指令的执行过程:

image.png

抛去赋值操作,++i实际执行过程只有一行字节码指令:

2: iinc    1, 1

转换成能理解的java代码的话,++i实际执行的就在局部变量中执行的:

i=i+1;

这么看来,在使用++i时确实比i++少了一步操作,少引入了一个局部变量,如果在运算结果相同的场景下,使用++i的话的确效率会比i++高那么一点点。

那么回到开头的问题,两种自增方式应用在for循环中执行的时候,那种效率更高呢?刚才得出的结论仍然适用于for循环中吗,别急,让我们接着往下看。

for循环中的自增

下面准备两段包含了for循环的代码,分别使用i++后置自增和++i前置自增:

//i++ 后置自增
public class ForIpp {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }
}
//++i 前置自增
public class ForPpi {
    public static void main(String[] args) {
        for (int i = 0; i < 5; ++i) {
            System.out.println(i);
        }
    }
}

老规矩,还是直接反编译后的字节码文件,然后对比一下指令的执行过程:

image.png

到这里,有趣的现象出现了,两段程序执行的字节码指令部分居然一模一样。先不考虑为什么会有这种现象,我们还是通过图解来看一下字节码指令的执行过程:

image.png

可以清晰的看到,在进行自增时,都是直接执行的iinc,在之前并没有执行iload的过程,也就是说,两段代码执行的都是++i。这一过程的验证其实还有更简单的方法,直接使用idea打开字节码文件,就可以看到最终for循环中使用的相同的前置自增方式。

image.png

那么,为什么会出现这种现象呢?归根结底,还是java编译器对于代码的优化,在两种自增方式中,如果没有赋值操作,那么都会被优化成一种方式,就像下面的两个方法的代码:

void ipp(){
    int i=3;
    i++;
}
void ppi(){
    int i=3;
    ++i;
}

最终执行时的字节码指令都是:

0: iconst_3
1: istore_1
2: iinc    1, 1
5: return

可以看到,在上面的这种特定情况下,代码经过编译器的优化,保持了语义不变,并通过转换语法的形式提高了代码的运行效率。所以再回到我们开头的问题,就可以得出结论,在for循环中,通过jvm进行编译优化后,不论是i++还是++i,最终执行的方式都是++i,因此执行效率是相同的。

所以,以后再碰到这种半吊子的面试官,和你谈for循环中i++++i的效率问题,自信点,直接把答案甩在他的脸上,两种方式效率一样!

image.png


相关文章
|
9月前
|
存储 Java
面试官:你真的搞清位运算了么?
面试官:你真的搞清位运算了么?
60 0
|
存储 算法
【手撕数据结构】(一)时间复杂度
【手撕数据结构】(一)时间复杂度
91 0
|
4月前
|
存储 搜索推荐 算法
【用Java学习数据结构系列】七大排序要悄咪咪的学(直接插入,希尔,归并,选择,堆排,冒泡,快排)以及计数排序(非比较排序)
【用Java学习数据结构系列】七大排序要悄咪咪的学(直接插入,希尔,归并,选择,堆排,冒泡,快排)以及计数排序(非比较排序)
40 1
|
4月前
【初阶数据结构】打破递归束缚:掌握非递归版快速排序与归并排序
【初阶数据结构】打破递归束缚:掌握非递归版快速排序与归并排序
|
搜索推荐
[数据结构 -- 手撕排序算法第七篇] 递归实现归并排序
[数据结构 -- 手撕排序算法第七篇] 递归实现归并排序
|
9月前
|
存储 算法
Leetcode 30天高效刷数据结构和算法 Day1 两数之和 —— 无序数组
给定一个无序整数数组和目标值,找出数组中和为目标值的两个数的下标。要求不重复且可按任意顺序返回。示例:输入nums = [2,7,11,15], target = 9,输出[0,1]。暴力解法时间复杂度O(n²),优化解法利用哈希表实现,时间复杂度O(n)。
56 0
|
存储 搜索推荐
[数据结构 -- 手撕排序算法第四篇] 堆排序,一篇带你搞懂堆排序
[数据结构 -- 手撕排序算法第四篇] 堆排序,一篇带你搞懂堆排序
|
9月前
|
搜索推荐 算法 Shell
【数据结构】手撕排序(排序的概念及意义、直接插入和希尔排序的实现及分析)
【数据结构】手撕排序(排序的概念及意义、直接插入和希尔排序的实现及分析)
|
9月前
|
算法 搜索推荐 C语言
C语言数据结构之排序整合与比较(冒泡,选择,插入,希尔,堆排序,快排及改良,归并排序,计数排序)
C语言数据结构之排序整合与比较(冒泡,选择,插入,希尔,堆排序,快排及改良,归并排序,计数排序)
|
9月前
|
机器学习/深度学习 搜索推荐 算法
程序员必须掌握的排序算法:希尔排序的原理与实现
程序员必须掌握的排序算法:希尔排序的原理与实现
138 1