Featured image of post Java 左值和右值的思考

Java 左值和右值的思考

Java 左值和右值的思考

JDK21

前言

算法题,用到小根堆「PriorityQueue」, 其中一个操作让我困惑了很久

小根堆存储的是原数组为负值的下标,则小根堆堆顶为最小负数的下标

本意是循环中,让最小负数取到相反数 k 次,变成一个正数

while (k-- > 0) nums[queue.peek()] = -nums[queue.poll()];

这一操作让我疑惑了很久,根据赋值表达式的性质,应该是从右计算到左,但是如果按照这个逻辑,就会让 poll() 先进行,后面才 peek() , 这样下标的计算就出错了, 即 peek() 实际取的下标值已经被 poll() 出堆了

问题复现

1
2
3
4
5
6
public static void main(String[] args) {
    int[] nums = {2, 1, 0};
    PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(0, 1, 2));
    nums[pq.peek()] = -nums[pq.poll()];
    System.out.println(Arrays.toString(nums));  // [-2, 1, 0]
}

小根存存储原数组下标,此时堆顶为 0

nums[pq.peek()] = -nums[pq.poll()] 实际执行运行时状态是先执行 peek(),再执行 poll(),即从左到右,与赋值运算符 = 右值赋值给左值不同

nums[0] = -nums[0] 取反

字节码分析

使用 javap -c Solution.class 反编译字节码分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
32: invokestatic  #15                 // InterfaceMethod java/util/List.of:(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/List;
35: invokespecial #21                 // Method java/util/PriorityQueue."<init>":(Ljava/util/Collection;)V
38: astore_2
39: aload_1
40: aload_2
41: invokevirtual #24                 // Method java/util/PriorityQueue.peek:()Ljava/lang/Object;
44: checkcast     #10                 // class java/lang/Integer
47: invokevirtual #28                 // Method java/lang/Integer.intValue:()I
50: aload_1
51: aload_2
52: invokevirtual #32                 // Method java/util/PriorityQueue.poll:()Ljava/lang/Object;
55: checkcast     #10                 // class java/lang/Integer
58: invokevirtual #28                 // Method java/lang/Integer.intValue:()I

先是堆的初始化,然后先执行 peek() 再执行 poll(), 即从左到右的顺序编译

其他实验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void main(String[] args) {
    int[] nums = {4, 5, 6};
    nums[getIndex()] = -nums[getValue()]; // Should call getIndex first, then getValue
    System.out.println(Arrays.toString(nums)); // [4, -6, 6]
}

public static int getIndex() {
    System.out.println("getIndex called");
    return 1;
}

public static int getValue() {
    System.out.println("getValue called");
    return 2;
}

getIndex called
getValue called
[4, -6, 6]

1
2
3
18: invokestatic  #7                  // Method getIndex:()I
21: aload_1
22: invokestatic  #13                 // Method getValue:()I

同样,编译顺序也是从左至右

Summary

  • 不能按照赋值表达式 = 从右向左运行的思想思考 nums[queue.peek()] = -nums[queue.poll()];
  • 最终的赋值的确会按照右向左执行「赋值操作」,完成 置相反数 的操作
  • 但是从最终字节码的执行顺序来看,对于 表达式 的计算,会 从左向右 计算,将前序准备工作「取值」完成后,才进行写操作,赋值。

🔗Reference

1005. K 次取反后最大化的数组和

使用 Hugo 构建
主题 StackJimmy 设计