Tại sao mảng [idx ++] + = Đây lại tăng idx một lần trong Java 8 nhưng hai lần trong Java 9 và 10?


751

Đối với một thách thức, một golfer mã đồng nghiệp đã viết mã sau đây :

import java.util.*;
public class Main {
  public static void main(String[] args) {
    int size = 3;
    String[] array = new String[size];
    Arrays.fill(array, "");
    for(int i = 0; i <= 100; ) {
      array[i++%size] += i + " ";
    }
    for(String element: array) {
      System.out.println(element);
    }
  }
}

Khi chạy mã này trong Java 8, chúng tôi nhận được kết quả sau:

1 4 7 10 13 16 19 22 25 28 31 34 37 40 43 46 49 52 55 58 61 64 67 70 73 76 79 82 85 88 91 94 97 100 
2 5 8 11 14 17 20 23 26 29 32 35 38 41 44 47 50 53 56 59 62 65 68 71 74 77 80 83 86 89 92 95 98 101 
3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99 

Khi chạy mã này trong Java 10, chúng tôi nhận được kết quả sau:

2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100 102 
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100 

Việc đánh số hoàn toàn tắt khi sử dụng Java 10. Vậy điều gì đang xảy ra ở đây? Đây có phải là một lỗi trong Java 10 không?

Theo dõi từ các ý kiến:

  • Vấn đề xuất hiện khi được biên dịch bằng Java 9 trở lên (chúng tôi đã tìm thấy nó trong Java 10). Biên dịch mã này trên Java 8, sau đó chạy trong Java 9 hoặc bất kỳ phiên bản nào mới hơn, bao gồm cả truy cập sớm Java 11, sẽ cho kết quả như mong đợi.
  • Loại mã này là không chuẩn, nhưng hợp lệ theo thông số kỹ thuật. Nó được Kevin Cruijssen tìm thấy trong một cuộc thảo luận trong một thử thách chơi gôn , do đó đã gặp phải trường hợp sử dụng kỳ lạ.
  • Didier L phát hiện ra rằng vấn đề có thể được sao chép với mã nhỏ hơn và dễ hiểu hơn nhiều:

    class Main {
      public static void main(String[] args) {
        String[] array = { "" };
        array[test()] += "a";
      }
      static int test() {
        System.out.println("evaluated");
        return 0;
      }
    }
    

    Kết quả khi được biên dịch trong Java 8:

    evaluated

    Kết quả khi được biên dịch trong Java 9 và 10:

    evaluated
    evaluated
    
  • Vấn đề này dường như bị giới hạn ở những nối chuỗi và toán tử gán ( +=) với một biểu thức với tác dụng phụ (s) là toán hạng bên trái, như trong array[test()]+="a", array[ix++]+="a", test()[index]+="a", hoặc test().field+="a". Để cho phép nối chuỗi, ít nhất một trong hai bên phải có kiểu String. Cố gắng tái tạo điều này trên các loại hoặc cấu trúc khác không thành công.


5
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

13
@JollyJoker Nó được giới hạn để +=áp dụng cho các Stringtài liệu tham khảo gián tiếp . Vì vậy, đầu tiên, mảng của bạn phải là một String[]. Vấn đề không xảy ra với int[], long[]và bạn bè. Nhưng vâng, về cơ bản là bạn đúng!
Olivier Grégoire

2
@ OlivierGrégoire mảng không cần phải có String[]. Nếu nó là Object[]và bạn làm array[expression] += "foo";, nó là như nhau. Nhưng có, nó không áp dụng cho mảng nguyên thủy, vì nó phải có khả năng giữ tham chiếu của kiểu String( Object[], CharSequence[], Comparable[], ...), để lưu trữ các kết quả của chuỗi nối.
Holger

30
Điều này đã được gán id lỗi JDK-8204322 .
Stuart Marks

1
@StuartMarks cảm ơn! Điều đó đã được tích hợp trong câu trả lời: Tôi thực sự muốn giữ câu hỏi một câu hỏi, về việc đó là bình thường hay là một lỗi. Mặc dù vậy, chúng ta có thể rõ ràng hơn về ID của lỗi trong câu trả lời. Tôi sẽ điều chỉnh nó ngay bây giờ.
Olivier Grégoire

Câu trả lời:


625

Đây là một lỗi khi javacbắt đầu từ JDK 9 (đã thực hiện một số thay đổi liên quan đến nối chuỗi, mà tôi nghi ngờ là một phần của vấn đề), như được xác nhận bởi javacnhóm theo id lỗi JDK-8204322 . Nếu bạn nhìn vào mã byte tương ứng cho dòng:

array[i++%size] += i + " ";

Nó là:

  21: aload_2
  22: iload_3
  23: iinc          3, 1
  26: iload_1
  27: irem
  28: aload_2
  29: iload_3
  30: iinc          3, 1
  33: iload_1
  34: irem
  35: aaload
  36: iload_3
  37: invokedynamic #5,  0 // makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
  42: aastore

Trong đó cuối cùng aaloadlà tải thực tế từ mảng. Tuy nhiên, phần

  21: aload_2             // load the array reference
  22: iload_3             // load 'i'
  23: iinc          3, 1  // increment 'i' (doesn't affect the loaded value)
  26: iload_1             // load 'size'
  27: irem                // compute the remainder

Mà gần tương ứng với biểu thức array[i++%size](trừ tải thực tế và lưu trữ), là trong đó hai lần. Điều này là không chính xác, như thông số kỹ thuật nói trong jls-15.26.2 :

Một biểu thức gán tổng hợp của biểu mẫu E1 op= E2tương đương với E1 = (T) ((E1) op (E2)), trong đó Tlà loại E1, ngoại trừ chỉ E1được đánh giá một lần.

Vì vậy, đối với biểu thức array[i++%size] += i + " ";, phần array[i++%size]chỉ nên được đánh giá một lần. Nhưng nó được đánh giá hai lần (một lần cho tải và một lần cho cửa hàng).

Vì vậy, có, đây là một lỗi.


Một số cập nhật:

Lỗi được sửa trong JDK 11 và sẽ có cổng sau cho JDK 10 (nhưng không phải là JDK 9, vì nó không còn nhận được các bản cập nhật công khai ).

Mitchsey Shipilev đề cập trên trang JBS (và @DidierL trong các bình luận ở đây):

Giải pháp thay thế: biên dịch với -XDstringConcat=inline

Điều đó sẽ trở lại việc sử dụng StringBuilderđể thực hiện nối và không có lỗi.


34
Nhân tiện, điều này áp dụng cho toàn bộ biểu thức phía bên trái, không chỉ chỉ mục cung cấp biểu thức phụ. Biểu thức này có thể phức tạp tùy ý. Xem ví dụ IntStream.range(0, 10) .peek(System.out::println).boxed().toArray()[0] += "";...
Holger

9
@Holger Phía bên tay trái thậm chí không cần liên quan đến mảng, vấn đề cũng xảy ra với một cách đơn giản test().field += "sth".
Didier L

44
Không phải là vấn đề, hành vi này là khủng khiếp phá vỡ dù sao, nhưng việc đánh giá đầu tiên là dành cho các cửa hàng và lần thứ hai cho tải, vì vậy array[index++] += "x";sẽ đọc từ array[index+1]và ghi vào array[index]...
Holger

5
@TheCoder Vâng tôi nghĩ vậy. JDK 9 không phải là bản phát hành hỗ trợ dài hạn (LTS). JDK 8 là và bản phát hành LTS tiếp theo là JDK 11. Xem tại đây: oracle.com/technetwork/java/javase/eol-135779.html Lưu ý rằng các cập nhật công khai cho JDK 9 đã kết thúc vào tháng 3.
Jorn Vernee

15
Trên JDK-8204322, Aleksey Shipilev đề nghị biên dịch -XDstringConcat=inlinenhư một cách giải quyết, cho những người cần nó.
Didier L
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.