Làm thế nào để nối chuỗi được thực hiện trong Java 9?


111

Như được viết trong JEP 280: Chỉ định kết nối chuỗi :

Thay đổi Stringchuỗi bytecode- định dạng tĩnh được tạo bởi javacđể sử dụng invokedynamiccác lệnh gọi đến các hàm thư viện JDK. Điều này sẽ cho phép tối ưu hóa quá trình Stringghép nối trong tương lai mà không yêu cầu thay đổi thêm đối với mã bytecode bị giới hạn bởi javac.

Ở đây tôi muốn hiểu việc sử dụng các invokedynamiccuộc gọi là gì và cách nối bytecode khác với invokedynamic?


11
Tôi đã viết về điều đó một thời gian trước - nếu điều đó hữu ích, tôi sẽ cô đọng nó thành một câu trả lời.
Nicolai

10
Ngoài ra, có một cái nhìn tại video này mà độc đáo giải thích quan điểm của cơ chế nối chuỗi mới: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov

3
@ZhekaKozlov Tôi ước tôi có thể bỏ phiếu hai lần cho nhận xét của bạn, các liên kết đến từ những người thực sự thực hiện tất cả những điều này là tốt nhất.
Eugene

2
@Nicolai: Điều đó thật tuyệt và sẽ là câu trả lời tốt hơn bất kỳ câu trả lời nào khác ở đây (kể cả của tôi). Bất kỳ phần nào trong câu trả lời của tôi mà bạn muốn kết hợp khi thực hiện, hãy thoải mái - nếu bạn bao gồm (về cơ bản) toàn bộ nội dung như một phần của câu trả lời rộng hơn, tôi sẽ chỉ xóa của tôi. Ngoài ra, nếu bạn muốn chỉ thêm vào câu trả lời của tôi vì nó khá hiển thị, tôi đã biến nó thành wiki cộng đồng.
TJ Crowder

Câu trả lời:


95

Cách "cũ" tạo ra một loạt các StringBuilderhoạt động được định hướng. Hãy xem xét chương trình này:

public class Example {
    public static void main(String[] args)
    {
        String result = args[0] + "-" + args[1] + "-" + args[2];
        System.out.println(result);
    }
}

Nếu chúng tôi biên dịch nó với JDK 8 hoặc cũ hơn và sau đó sử dụng javap -c Exampleđể xem mã bytecode, chúng tôi sẽ thấy một cái gì đó như thế này:

Ví dụ về lớp công khai {
  public Ví dụ ();
    Mã:
       0: aload_0
       1: invokespecial # 1 // Phương thức java / lang / Object. "<init>" :() V
       4: trở lại

  public static void main (java.lang.String []);
    Mã:
       0: new # 2 // class java / lang / StringBuilder
       3: trùng lặp
       4: invokespecial # 3 // Phương thức java / lang / StringBuilder. "<init>" :() V
       7: aload_0
       8: biểu tượngt_0
       9: aaload
      10: invokevirtual # 4 // Phương thức java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      13: ldc # 5 // Chuỗi -
      15: invokevirtual # 4 // Phương thức java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      18: aload_0
      19: biểu tượngt_1
      20: aaload
      21: invokevirtual # 4 // Phương thức java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      24: ldc # 5 // Chuỗi -
      26: invokevirtual # 4 // Phương thức java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      29: aload_0
      30: biểu tượngt_2
      31: aaload
      32: invokevirtual # 4 // Phương thức java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      35: invokevirtual # 6 // Phương thức java / lang / StringBuilder.toString :() Ljava / lang / String;
      38: astore_1
      39: getstatic # 7 // Trường java / lang / System.out: Ljava / io / PrintStream;
      42: aload_1
      43: invokevirtual # 8 // Phương thức java / io / PrintStream.println: (Ljava / lang / String;) V
      46: trở lại
}

Như bạn có thể thấy, nó tạo ra một StringBuildervà sử dụng append. Điều này nổi tiếng là khá kém hiệu quả vì dung lượng mặc định của bộ đệm tích hợp StringBuilderchỉ là 16 ký tự và không có cách nào để trình biên dịch biết trước để phân bổ thêm, vì vậy nó sẽ phải phân bổ lại. Nó cũng là một loạt các cuộc gọi phương thức. (Tuy nhiên, lưu ý rằng JVM đôi khi có thể phát hiện và viết lại các mẫu lệnh gọi này để làm cho chúng hiệu quả hơn.)

Hãy xem những gì Java 9 tạo ra:

Ví dụ về lớp công khai {
  public Ví dụ ();
    Mã:
       0: aload_0
       1: invokespecial # 1 // Phương thức java / lang / Object. "<init>" :() V
       4: trở lại

  public static void main (java.lang.String []);
    Mã:
       0: aload_0
       1: biểu tượngt_0
       2: aaload
       3: aload_0
       4: biểu tượngt_1
       5: aaload
       6: aload_0
       7: biểu tượngt_2
       8: aaload
       9: invokedynamic # 2, 0 // InvokeDynamic # 0: makeConcatWithConstants: (Ljava / lang / String; Ljava / lang / String; Ljava / lang / String;) Ljava / lang / String;
      14: astore_1
      15: getstatic # 3 // Trường java / lang / System.out: Ljava / io / PrintStream;
      18: aload_1
      19: invokevirtual # 4 // Phương thức java / io / PrintStream.println: (Ljava / lang / String;) V
      22: trở lại
}

Ôi trời nhưng ngắn hơn. :-) Nó thực hiện một cuộc gọi duy nhất đến makeConcatWithConstantstừ StringConcatFactory, nói điều này trong Javadoc của nó:

Các phương pháp để tạo điều kiện thuận lợi cho việc tạo ra các phương pháp nối chuỗi, có thể được sử dụng để nối một cách hiệu quả một số lượng đối số đã biết của các kiểu đã biết, có thể sau khi điều chỉnh kiểu và đánh giá một phần các đối số. Các phương thức này thường được sử dụng làm phương thức khởi động cho invokedynamiccác trang web cuộc gọi, để hỗ trợ tính năng nối chuỗi của Ngôn ngữ lập trình Java.


41
Điều này làm tôi nhớ lại câu trả lời mà tôi đã viết gần 6 năm trước cho đến ngày hôm nay: stackoverflow.com/a/7586780/330057 - Ai đó đã hỏi liệu họ có nên tạo một StringBuilder hay chỉ sử dụng đơn giản cũ +=trong vòng lặp for của họ. Tôi đã nói với họ rằng điều đó còn tùy thuộc, nhưng chúng ta đừng quên rằng một lúc nào đó họ có thể tìm ra cách tốt hơn để xâu chuỗi concat. Điểm mấu chốt thực sự là dòng áp chót:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa

3
@corsiKa: LOL! Nhưng wow, phải mất một thời gian dài để đạt được điều đó (tôi không có nghĩa là sáu năm, tôi có nghĩa là 22 hoặc lâu hơn ... :-))
TJ Crowder

1
@supercat: Theo tôi hiểu, có một vài lý do, đặc biệt là việc tạo một mảng varargs để chuyển tới một phương thức trên đường dẫn quan trọng về hiệu suất không phải là lý tưởng. Ngoài ra, việc sử dụng invokedynamiccho phép các chiến lược nối khác nhau được chọn trong thời gian chạy và bị ràng buộc trong lần gọi đầu tiên, mà không cần gọi phương thức và bảng điều phối trên mỗi lần gọi; nhiều hơn trong bài báo của nicolai tại đây và trong JEP .
TJ Crowder

1
@supercat: Và có một thực tế là nó sẽ không hoạt động tốt với không phải Chuỗi, vì chúng sẽ phải được chuyển đổi trước thành Chuỗi thay vì được chuyển thành kết quả cuối cùng; kém hiệu quả hơn. Có thể làm cho nó Object, nhưng sau đó bạn sẽ phải hộp tất cả các nguyên thủy ... (nào Nicolai bìa trong bài viết xuất sắc của mình, btw.)
TJ Crowder

2
@supercat Tôi đang đề cập đến String.concat(String)phương thức đã tồn tại mà việc triển khai đang tạo mảng của chuỗi kết quả tại chỗ. Lợi thế trở nên tranh luận khi chúng ta phải gọi toString()trên các đối tượng tùy ý. Tương tự như vậy, khi gọi một phương thức chấp nhận một mảng, người gọi phải tạo và lấp đầy mảng, điều này làm giảm lợi ích tổng thể. Nhưng bây giờ, nó không liên quan, vì giải pháp mới về cơ bản là những gì bạn đang xem xét, ngoại trừ việc nó không có phí quyền anh, không cần tạo mảng và phần phụ trợ có thể tạo ra các trình xử lý được tối ưu hóa cho các tình huống cụ thể.
Holger

20

Trước khi đi vào chi tiết của việc invokedynamictriển khai được sử dụng để tối ưu hóa việc nối chuỗi, theo ý kiến ​​của tôi, người ta phải nắm được một số thông tin cơ bản về What invokedynamic và làm cách nào để sử dụng nó?

Các invokedynamic đơn giản hoá hướng dẫn và có khả năng cải thiện triển khai của trình biên dịch và hệ thống thời gian chạy cho các ngôn ngữ động trên JVM . Nó thực hiện điều này bằng cách cho phép người triển khai ngôn ngữ xác định hành vi liên kết tùy chỉnh với invokedynamichướng dẫn bao gồm các bước sau.


Tôi có thể sẽ thử và hướng dẫn bạn qua những điều này với những thay đổi được đưa ra cùng với việc triển khai tối ưu hóa nối chuỗi.

  • Định nghĩa phương pháp Bootstrap : - Với Java9, các phương thức bootstrap cho invokedynamiccác site gọi, để hỗ trợ chủ yếu cho việc nối chuỗi makeConcatmakeConcatWithConstantsđã được giới thiệu cùng với việc StringConcatFactorytriển khai.

    Việc sử dụng invokedynamic cung cấp một giải pháp thay thế để chọn chiến lược dịch cho đến thời gian chạy. Chiến lược dịch được sử dụng trong StringConcatFactorytương tự như chiến lược đã LambdaMetafactorygiới thiệu trong phiên bản java trước đó. Ngoài ra, một trong những mục tiêu của JEP được đề cập trong câu hỏi là mở rộng các chiến lược này hơn nữa.

  • Chỉ định các mục nhập liên tục trong nhóm : - Đây là các đối số tĩnh bổ sung cho invokedynamiclệnh khác với (1) MethodHandles.Lookupđối tượng là nhà máy để tạo các xử lý phương thức trong ngữ cảnh của invokedynamiclệnh, (2) một Stringđối tượng, tên phương thức được đề cập trong lệnh gọi động site và (3) MethodTypeđối tượng, chữ ký kiểu đã phân giải của site gọi động.

    Đã có liên kết trong quá trình liên kết của mã. Trong thời gian chạy, phương thức bootstrap chạy và liên kết trong mã thực thực hiện quá trình nối. Nó viết lại invokedynamiccuộc gọi bằng một invokestaticcuộc gọi thích hợp . Thao tác này tải chuỗi không đổi từ nhóm hằng số, các args tĩnh của phương thức bootstrap được tận dụng để chuyển các hằng số này và các hằng số khác thẳng đến lệnh gọi phương thức bootstrap.

  • Sử dụng Lệnh gọi động : - Lệnh này cung cấp các cơ sở cho liên kết lười biếng, bằng cách cung cấp phương tiện để khởi động mục tiêu cuộc gọi một lần, trong lần gọi đầu tiên. Ý tưởng cụ thể để tối ưu hóa ở đây là thay thế toàn bộ StringBuilder.appendvũ điệu bằng một invokedynamiclệnh gọi đơn giản đến java.lang.invoke.StringConcatFactory, điều đó sẽ chấp nhận các giá trị cần nối.

Các Indify String nối các quốc gia đề nghị với một ví dụ điểm chuẩn của các ứng dụng với Java9 nơi một phương pháp tương tự như chia sẻ bởi @TJ Crowder được biên dịch và sự khác biệt trong bytecode là khá rõ ràng giữa việc thực hiện khác nhau.


17

Tôi sẽ thêm một chút chi tiết ở đây. Phần chính cần có là cách nối chuỗi được thực hiện là quyết định thời gian chạy, không phải là thời gian biên dịch nữa . Do đó, nó có thể thay đổi, có nghĩa là bạn đã biên dịch mã của mình một lần so với java-9 và nó có thể thay đổi cách triển khai bên dưới theo cách nào nó vừa ý mà không cần phải biên dịch lại.

Và điểm thứ hai là hiện tại có 6 possible strategies for concatenation of String:

 private enum Strategy {
    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder}.
     */
    BC_SB,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but trying to estimate the required storage.
     */
    BC_SB_SIZED,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but computing the required storage exactly.
     */
    BC_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also tries to estimate the required storage.
     */
    MH_SB_SIZED,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also estimate the required storage exactly.
     */
    MH_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that constructs its own byte[] array from
     * the arguments. It computes the required storage exactly.
     */
    MH_INLINE_SIZED_EXACT
}

Bạn có thể chọn bất kỳ của họ thông qua một tham số: -Djava.lang.invoke.stringConcat. Lưu ý rằng đó StringBuildervẫn là một tùy chọn.

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.