Các tính năng mã byte không có sẵn trong ngôn ngữ Java


146

Hiện tại có (Java 6) những điều bạn có thể làm trong mã byte Java mà bạn không thể thực hiện từ bên trong ngôn ngữ Java không?

Tôi biết cả hai đều hoàn thành Turing, vì vậy hãy đọc "có thể làm" vì "có thể làm nhanh hơn / tốt hơn đáng kể hoặc theo một cách khác".

Tôi đang nghĩ về các mã byte bổ sung như invokedynamic, không thể được tạo bằng Java, ngoại trừ một mã cụ thể dành cho phiên bản tương lai.


3
Xác định "những thứ". Cuối cùng, ngôn ngữ Java và mã byte Java đều hoàn thành Turing ...
Michael Borgwardt

2
Là câu hỏi thực sự; Có bất kỳ lập trình lợi thế nào trong mã byte, ví dụ như sử dụng Jasmin, thay vì Java?
Peter Lawrey

2
Giống như roltrong trình biên dịch chương trình, bạn không thể viết bằng C ++.
Martijn Courteaux

1
Đó là một trình biên dịch tối ưu hóa rất kém mà không thể biên dịch (x<<n)|(x>>(32-n))thành một rollệnh.
Random832

Câu trả lời:


62

Theo như tôi biết, không có các tính năng chính trong các mã byte được Java 6 hỗ trợ mà mã nguồn Java không thể truy cập được. Lý do chính cho điều này rõ ràng là mã byte Java được thiết kế với ngôn ngữ Java.

Tuy nhiên, có một số tính năng không được sản xuất bởi các trình biên dịch Java hiện đại:

  • Các ACC_SUPERlá cờ :

    Đây là một cờ có thể được đặt trên một lớp và chỉ định cách invokespecialxử lý một trường hợp góc cụ thể của mã byte được xử lý cho lớp này. Nó được thiết lập bởi tất cả các trình biên dịch Java hiện đại (trong đó "hiện đại" là> = Java 1.1, nếu tôi nhớ chính xác) và chỉ các trình biên dịch Java cổ đại đã tạo ra các tệp lớp trong đó điều này chưa được đặt. Cờ này chỉ tồn tại vì lý do tương thích ngược. Lưu ý rằng bắt đầu với Java 7u51, ACC_SUPER bị bỏ qua hoàn toàn vì lý do bảo mật.

  • Các jsr/ retmã byte.

    Các mã byte này được sử dụng để thực hiện các thường trình con (chủ yếu để thực hiện finallycác khối). Chúng không còn được sản xuất kể từ Java 6 . Lý do cho sự phản đối của họ là vì họ làm phức tạp việc xác minh tĩnh rất nhiều vì không có lợi ích lớn (tức là mã sử dụng hầu như luôn có thể được thực hiện lại với các bước nhảy bình thường với rất ít chi phí).

  • Có hai phương thức trong một lớp chỉ khác nhau về kiểu trả về.

    Đặc tả ngôn ngữ Java không cho phép hai phương thức trong cùng một lớp khi chúng chỉ khác nhau về kiểu trả về của chúng (nghĩa là cùng tên, cùng danh sách đối số, ...). Tuy nhiên, đặc tả JVM không có hạn chế như vậy, vì vậy một tệp lớp có thể chứa hai phương thức như vậy, không có cách nào để tạo ra một tệp lớp như vậy bằng trình biên dịch Java thông thường. Có một ví dụ hay giải thích trong câu trả lời này .


5
Tôi có thể thêm một câu trả lời khác, nhưng chúng tôi cũng có thể làm cho bạn câu trả lời chính tắc. Bạn có thể muốn đề cập rằng chữ ký của một phương thức trong mã byte bao gồm kiểu trả về . Nghĩa là, bạn có thể có hai phương thức với các loại tham số chính xác giống nhau, nhưng các kiểu trả về khác nhau. Xem cuộc thảo luận này: stackoverflow.com/questions/3110014/is-this-valid-java/ mẹo
Adam Paynter

8
Bạn có thể có tên lớp, phương thức và trường với bất kỳ ký tự nào. Tôi đã làm việc trong một dự án trong đó các "trường" có dấu cách và dấu gạch ngang trong tên của chúng. : P
Peter Lawrey

3
@Peter: Nói về các ký tự hệ thống tệp, tôi tình cờ gặp một obfuscator đã đổi tên một lớp thành một lớp akhác Atrong tệp JAR. Tôi mất khoảng nửa giờ để giải nén trên máy Windows trước khi tôi nhận ra các lớp bị thiếu ở đâu. :)
Adam Paynter

3
@JoachimSauer: diễn giải JVM spec, trang 75: tên lớp, phương pháp, lĩnh vực, và các biến cục bộ có thể chứa bất kỳ nhân vật ngoại trừ '.', ';', '[', hoặc '/'. Tên phương thức giống nhau, nhưng chúng cũng không thể chứa '<'hoặc '>'. (Với các trường hợp ngoại lệ đáng chú ý <init><clinit>ví dụ và các hàm tạo tĩnh.) Tôi nên chỉ ra rằng nếu bạn tuân thủ nghiêm ngặt đặc tả, các tên lớp thực sự bị ràng buộc nhiều hơn, nhưng các ràng buộc không được thi hành.
leviathanbadger

3
@JoachimSauer: cũng là một bổ sung không có giấy tờ của riêng tôi: ngôn ngữ java bao gồm "throws ex1, ex2, ..., exn"như là một phần của chữ ký phương thức; bạn không thể thêm mệnh đề ném ngoại lệ vào các phương thức được ghi đè. NHƯNG, JVM không thể quan tâm ít hơn. Vì vậy, chỉ có finalcác phương thức thực sự được JVM bảo đảm là không có ngoại lệ - tất nhiên ngoài RuntimeExceptions và Errors. Quá nhiều cho việc xử lý ngoại lệ được kiểm tra: D
leviathanbadger

401

Sau khi làm việc với mã byte Java khá lâu và thực hiện một số nghiên cứu bổ sung về vấn đề này, đây là tóm tắt các phát hiện của tôi:

Thực thi mã trong hàm tạo trước khi gọi siêu xây dựng hoặc hàm tạo phụ trợ

Trong ngôn ngữ lập trình Java (JPL), câu lệnh đầu tiên của hàm tạo phải là một lời gọi của siêu kiến ​​trúc hoặc một hàm tạo khác của cùng một lớp. Điều này không đúng với mã byte Java (JBC). Trong mã byte, việc thực thi bất kỳ mã nào trước một hàm tạo là hoàn toàn hợp pháp, miễn là:

  • Một hàm tạo tương thích khác được gọi vào một lúc nào đó sau khối mã này.
  • Cuộc gọi này không nằm trong một tuyên bố có điều kiện.
  • Trước lệnh gọi hàm tạo này, không có trường nào của thể hiện được xây dựng được đọc và không có phương thức nào của nó được gọi. Điều này ngụ ý các mục tiếp theo.

Đặt các trường đối tượng trước khi gọi siêu xây dựng hoặc hàm tạo phụ trợ

Như đã đề cập trước đây, việc đặt giá trị trường của một thể hiện trước khi gọi một hàm tạo khác là hoàn toàn hợp pháp. Thậm chí còn tồn tại một bản hack kế thừa giúp nó có thể khai thác "tính năng" này trong các phiên bản Java trước 6:

class Foo {
  public String s;
  public Foo() {
    System.out.println(s);
  }
}

class Bar extends Foo {
  public Bar() {
    this(s = "Hello World!");
  }
  private Bar(String helper) {
    super();
  }
}

Theo cách này, một trường có thể được đặt trước khi siêu xây dựng được gọi mà không thể thực hiện được nữa. Trong JBC, hành vi này vẫn có thể được thực hiện.

Chi nhánh một cuộc gọi siêu xây dựng

Trong Java, không thể định nghĩa một lệnh gọi constructor như

class Foo {
  Foo() { }
  Foo(Void v) { }
}

class Bar() {
  if(System.currentTimeMillis() % 2 == 0) {
    super();
  } else {
    super(null);
  }
}

Cho đến Java 7u23, trình xác minh của HotSpot VM đã bỏ qua kiểm tra này, đó là lý do có thể. Điều này đã được sử dụng bởi một số công cụ tạo mã như là một loại hack nhưng nó không còn hợp pháp để thực hiện một lớp như thế này.

Cái sau chỉ là một lỗi trong phiên bản trình biên dịch này. Trong các phiên bản trình biên dịch mới hơn, điều này một lần nữa có thể.

Xác định một lớp mà không có bất kỳ hàm tạo nào

Trình biên dịch Java sẽ luôn luôn triển khai ít nhất một hàm tạo cho bất kỳ lớp nào. Trong mã byte Java, điều này là không bắt buộc. Điều này cho phép tạo ra các lớp không thể được xây dựng ngay cả khi sử dụng sự phản chiếu. Tuy nhiên, sử dụng sun.misc.Unsafevẫn cho phép tạo ra các trường hợp như vậy.

Xác định các phương thức có chữ ký giống hệt nhau nhưng với kiểu trả về khác nhau

Trong JPL, một phương thức được xác định là duy nhất bởi tên và các loại tham số thô của nó. Trong JBC, loại lợi nhuận thô được xem xét bổ sung.

Xác định các trường không khác nhau theo tên mà chỉ theo loại

Một tệp lớp có thể chứa một số trường có cùng tên miễn là chúng khai báo một loại trường khác. JVM luôn đề cập đến một trường như một bộ tên và loại.

Ném ngoại lệ được kiểm tra không khai báo mà không bắt chúng

Thời gian chạy Java và mã byte Java không nhận thức được khái niệm về các ngoại lệ được kiểm tra. Chỉ có trình biên dịch Java xác minh rằng các ngoại lệ được kiểm tra luôn luôn bị bắt hoặc khai báo nếu chúng bị ném.

Sử dụng lời gọi phương thức động bên ngoài các biểu thức lambda

Cái gọi là gọi phương thức động có thể được sử dụng cho mọi thứ, không chỉ cho các biểu thức lambda của Java. Sử dụng tính năng này cho phép ví dụ để tắt logic thực thi khi chạy. Nhiều ngôn ngữ lập trình động giúp JBC cải thiện hiệu suất của chúng bằng cách sử dụng hướng dẫn này. Trong mã byte Java, bạn cũng có thể mô phỏng các biểu thức lambda trong Java 7 nơi trình biên dịch chưa cho phép sử dụng lời gọi phương thức động trong khi JVM đã hiểu hướng dẫn.

Sử dụng định danh thường không được coi là hợp pháp

Bạn đã bao giờ sử dụng khoảng trắng và ngắt dòng trong tên phương thức của mình chưa? Tạo JBC của riêng bạn và chúc may mắn để xem xét mã. Các ký tự bất hợp pháp cho định danh là ., ;, [/. Ngoài ra, các phương thức không được đặt tên <init>hoặc <clinit>không thể chứa <>.

Xác định lại finalcác tham số hoặc thistham chiếu

finalcác tham số không tồn tại trong JBC và do đó có thể được gán lại. Bất kỳ tham số nào, bao gồm thistham chiếu chỉ được lưu trữ trong một mảng đơn giản trong JVM, điều này cho phép gán lại thistham chiếu tại chỉ mục 0trong một khung phương thức duy nhất.

Tái chỉ định finalcác lĩnh vực

Miễn là trường cuối cùng được gán trong hàm tạo, việc gán lại giá trị này hoặc thậm chí không gán giá trị là hợp pháp. Do đó, hai nhà xây dựng sau đây là hợp pháp:

class Foo {
  final int bar;
  Foo() { } // bar == 0
  Foo(Void v) { // bar == 2
    bar = 1;
    bar = 2;
  }
}

Đối với static finalcác trường, nó thậm chí còn được phép gán lại các trường bên ngoài trình khởi tạo lớp.

Đối xử với các hàm tạo và trình khởi tạo lớp như thể chúng là các phương thức

Đây là một tính năng mang thai nhiều hơn nhưng các nhà xây dựng không được đối xử khác biệt trong JBC so với các phương pháp thông thường. Chỉ có trình xác minh của JVM đảm bảo rằng các hàm tạo gọi một hàm tạo hợp pháp khác. Ngoài ra, nó chỉ là một quy ước đặt tên Java mà các hàm tạo phải được gọi <init>và trình khởi tạo lớp được gọi <clinit>. Bên cạnh sự khác biệt này, việc biểu diễn các phương thức và hàm tạo là giống hệt nhau. Như Holger đã chỉ ra trong một nhận xét, bạn thậm chí có thể định nghĩa các hàm tạo với các kiểu trả về khác với voidhoặc một trình khởi tạo lớp bằng các đối số, mặc dù không thể gọi các phương thức này.

Tạo hồ sơ bất đối xứng * .

Khi tạo một bản ghi

record Foo(Object bar) { }

javac sẽ tạo một tệp lớp với một trường duy nhất có tên bar, một phương thức truy cập có tên bar()và một hàm tạo lấy một Object. Ngoài ra, một thuộc tính bản ghi cho barđược thêm vào. Bằng cách tạo thủ công một bản ghi, có thể tạo, một hình dạng hàm tạo khác nhau, bỏ qua trường và triển khai trình truy cập khác nhau. Đồng thời, vẫn có thể làm cho API phản chiếu tin rằng lớp đại diện cho một bản ghi thực tế.

Gọi bất kỳ phương thức siêu nào (cho đến Java 1.1)

Tuy nhiên, điều này chỉ có thể cho các phiên bản Java 1 và 1.1. Trong JBC, các phương thức luôn được gửi trên một loại mục tiêu rõ ràng. Điều này có nghĩa là cho

class Foo {
  void baz() { System.out.println("Foo"); }
}

class Bar extends Foo {
  @Override
  void baz() { System.out.println("Bar"); }
}

class Qux extends Bar {
  @Override
  void baz() { System.out.println("Qux"); }
}

nó có thể thực hiện Qux#bazđể gọi Foo#baztrong khi nhảy qua Bar#baz. Mặc dù vẫn có thể định nghĩa một lời gọi rõ ràng để gọi một triển khai siêu phương thức khác so với siêu lớp trực tiếp, nhưng điều này không còn có tác dụng gì trong các phiên bản Java sau 1.1. Trong Java 1.1, hành vi này được kiểm soát bằng cách đặt ACC_SUPERcờ sẽ cho phép hành vi tương tự chỉ gọi việc triển khai của lớp siêu trực tiếp.

Xác định một cuộc gọi không ảo của một phương thức được khai báo trong cùng một lớp

Trong Java, không thể định nghĩa một lớp

class Foo {
  void foo() {
    bar();
  }
  void bar() { }
}

class Bar extends Foo {
  @Override void bar() {
    throw new RuntimeException();
  }
}

Đoạn mã trên sẽ luôn luôn dẫn đến một RuntimeExceptionkhi foođược gọi trong một thể hiện của Bar. Không thể định nghĩa Foo::foophương thức để gọi phương thức của chính bar nó được định nghĩa trong Foo. Là barmột phương thức cá nhân không riêng tư, cuộc gọi luôn ảo. Tuy nhiên, với mã byte, người ta có thể định nghĩa lời gọi sử dụng INVOKESPECIALopcode liên kết trực tiếp barlời gọi phương thức Foo::foovới Foophiên bản của. Opcode này thường được sử dụng để thực hiện các yêu cầu siêu phương thức nhưng bạn có thể sử dụng lại opcode để thực hiện hành vi được mô tả.

Chú thích loại hạt mịn

Trong Java, các chú thích được áp dụng theo chúng @Targetmà các chú thích khai báo. Sử dụng thao tác mã byte, có thể xác định các chú thích độc lập với điều khiển này. Ngoài ra, ví dụ có thể chú thích một loại tham số mà không chú thích tham số ngay cả khi @Targetchú thích áp dụng cho cả hai thành phần.

Xác định bất kỳ thuộc tính nào cho một loại hoặc các thành viên của nó

Trong ngôn ngữ Java, chỉ có thể định nghĩa các chú thích cho các trường, phương thức hoặc lớp. Trong JBC, về cơ bản bạn có thể nhúng bất kỳ thông tin nào vào các lớp Java. Tuy nhiên, để sử dụng thông tin này, bạn không còn có thể dựa vào cơ chế tải lớp Java mà bạn cần phải tự trích xuất thông tin meta.

Tràn và ngầm assign byte, short, charbooleancác giá trị

Các kiểu nguyên thủy thứ hai thường không được biết đến trong JBC mà chỉ được xác định cho các kiểu mảng hoặc cho các mô tả trường và phương thức. Trong các hướng dẫn mã byte, tất cả các loại được đặt tên đều có khoảng trống 32 bit cho phép biểu diễn chúng dưới dạng int. Chính thức, chỉ có int, float, longdoublecác loại tồn tại trong mã byte đó tất cả các nhu cầu chuyển đổi rõ ràng bởi sự cai trị của người xác minh của JVM.

Không phát hành màn hình

Một synchronizedkhối thực sự được tạo thành từ hai tuyên bố, một để thu nhận và một để phát hành màn hình. Trong JBC, bạn có thể có được một cái mà không cần phát hành nó.

Lưu ý : Trong các triển khai gần đây của HotSpot, điều này thay vào đó dẫn đến IllegalMonitorStateExceptionviệc kết thúc một phương thức hoặc phát hành ngầm nếu phương thức bị chấm dứt bởi chính một ngoại lệ.

Thêm nhiều returncâu lệnh vào trình khởi tạo kiểu

Trong Java, ngay cả một trình khởi tạo kiểu tầm thường như

class Foo {
  static {
    return;
  }
}

Là bất hợp pháp. Trong mã byte, trình khởi tạo kiểu được xử lý giống như bất kỳ phương thức nào khác, tức là các câu lệnh return có thể được định nghĩa ở bất cứ đâu.

Tạo các vòng lặp không thể giảm

Trình biên dịch Java chuyển đổi các vòng lặp thành các câu lệnh goto trong mã byte Java. Các câu lệnh như vậy có thể được sử dụng để tạo các vòng lặp không thể sửa chữa, điều mà trình biên dịch Java không bao giờ làm được.

Xác định một khối bắt đệ quy

Trong mã byte Java, bạn có thể định nghĩa một khối:

try {
  throw new Exception();
} catch (Exception e) {
  <goto on exception>
  throw Exception();
}

Một câu lệnh tương tự được tạo ra hoàn toàn khi sử dụng một synchronizedkhối trong Java trong đó bất kỳ ngoại lệ nào trong khi phát hành một màn hình sẽ trả về hướng dẫn để phát hành màn hình này. Thông thường, không có ngoại lệ nào xảy ra trên một hướng dẫn như vậy nhưng nếu nó (ví dụ như không dùng nữa ThreadDeath), màn hình vẫn sẽ được phát hành.

Gọi bất kỳ phương thức mặc định

Trình biên dịch Java yêu cầu một số điều kiện phải được thực hiện để cho phép gọi phương thức mặc định:

  1. Phương thức phải là phương thức cụ thể nhất (không được ghi đè bởi giao diện phụ được thực hiện bởi bất kỳ loại nào , kể cả các loại siêu).
  2. Loại giao diện của phương thức mặc định phải được triển khai trực tiếp bởi lớp đang gọi phương thức mặc định. Tuy nhiên, nếu giao diện Bmở rộng giao diện Anhưng không ghi đè phương thức A, phương thức vẫn có thể được gọi.

Đối với mã byte Java, chỉ có điều kiện thứ hai được tính. Điều đầu tiên là không liên quan.

Gọi một siêu phương thức trên một cá thể không phải là this

Trình biên dịch Java chỉ cho phép gọi một phương thức siêu (hoặc mặc định giao diện) trong các trường hợp của this. Tuy nhiên, trong mã byte, cũng có thể gọi siêu phương thức trên một thể hiện cùng loại tương tự như sau:

class Foo {
  void m(Foo f) {
    f.super.toString(); // calls Object::toString
  }
  public String toString() {
    return "foo";
  }
}

Truy cập thành viên tổng hợp

Trong mã byte Java, có thể truy cập trực tiếp các thành viên tổng hợp. Ví dụ, hãy xem xét làm thế nào trong ví dụ sau đây, ví dụ bên ngoài của một Barthể hiện khác được truy cập:

class Foo {
  class Bar { 
    void bar(Bar bar) {
      Foo foo = bar.Foo.this;
    }
  }
}

Điều này thường đúng cho bất kỳ lĩnh vực, lớp hoặc phương pháp tổng hợp nào.

Xác định thông tin loại chung không đồng bộ

Mặc dù thời gian chạy Java không xử lý các kiểu chung (sau khi trình biên dịch Java áp dụng kiểu xóa), thông tin này vẫn được chuyển đến một lớp được biên dịch dưới dạng thông tin meta và có thể truy cập thông qua API phản chiếu.

Trình xác minh không kiểm tra tính nhất quán của các Stringgiá trị được mã hóa dữ liệu meta này . Do đó, có thể xác định thông tin về các loại chung không phù hợp với việc xóa. Như một sự thuyết phục, những khẳng định sau đây có thể đúng:

Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());

Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);

Ngoài ra, chữ ký có thể được định nghĩa là không hợp lệ sao cho ngoại lệ thời gian chạy bị ném. Ngoại lệ này được đưa ra khi thông tin được truy cập lần đầu tiên vì nó được đánh giá một cách lười biếng. (Tương tự như các giá trị chú thích có lỗi.)

Chỉ thêm thông tin meta tham số cho các phương thức nhất định

Trình biên dịch Java cho phép nhúng tên tham số và thông tin sửa đổi khi biên dịch một lớp với parametercờ được kích hoạt. Trong định dạng tệp lớp Java, tuy nhiên thông tin này được lưu trữ theo phương thức, điều này cho phép chỉ có thể nhúng thông tin phương thức đó cho các phương thức nhất định.

Mọi thứ rối tung và làm hỏng JVM của bạn

Ví dụ, trong mã byte Java, bạn có thể định nghĩa để gọi bất kỳ phương thức nào trên bất kỳ loại nào. Thông thường, trình xác minh sẽ khiếu nại nếu một loại không biết phương thức đó. Tuy nhiên, nếu bạn gọi một phương thức không xác định trên một mảng, tôi đã tìm thấy một lỗi trong một số phiên bản JVM trong đó trình xác minh sẽ bỏ lỡ điều này và JVM của bạn sẽ kết thúc sau khi lệnh được gọi. Đây hầu như không phải là một tính năng, nhưng về mặt kỹ thuật là điều không thể với Java được biên dịch javac . Java có một số loại xác nhận hợp lệ. Việc xác thực đầu tiên được áp dụng bởi trình biên dịch Java, lần thứ hai bởi JVM khi một lớp được tải. Bằng cách bỏ qua trình biên dịch, bạn có thể tìm thấy một điểm yếu trong xác thực của trình xác minh. Đây là một tuyên bố chung hơn là một tính năng, mặc dù.

Chú thích loại máy thu của nhà xây dựng khi không có lớp bên ngoài

Kể từ Java 8, các phương thức và hàm tạo không tĩnh của các lớp bên trong có thể khai báo một kiểu người nhận và chú thích các kiểu này. Các nhà xây dựng của các lớp cấp cao nhất không thể chú thích loại người nhận của họ vì hầu hết họ không khai báo một loại.

class Foo {
  class Bar {
    Bar(@TypeAnnotation Foo Foo.this) { }
  }
  Foo() { } // Must not declare a receiver type
}

Kể từ Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()tuy nhiên không trả về một AnnotatedTypeđại diện Foo, nó có thể bao gồm loại chú thích cho Foo's constructor trực tiếp trong tập tin lớp nơi các chú thích được sau đọc bởi API phản chiếu.

Sử dụng các hướng dẫn mã byte không sử dụng / kế thừa

Vì những người khác đặt tên cho nó, tôi cũng sẽ bao gồm nó. Java trước đây đã sử dụng các chương trình con bởi các câu lệnh JSRRET. JBC thậm chí còn biết loại địa chỉ trả lại cho mục đích này. Tuy nhiên, việc sử dụng các chương trình con đã làm quá mức phân tích mã tĩnh, đó là lý do tại sao các hướng dẫn này không còn được sử dụng. Thay vào đó, trình biên dịch Java sẽ sao chép mã mà nó biên dịch. Tuy nhiên, điều này về cơ bản tạo ra logic giống hệt nhau, đó là lý do tại sao tôi không thực sự xem xét nó để đạt được điều gì đó khác biệt. Tương tự, ví dụ, bạn có thể thêmNOOPHướng dẫn mã byte không được trình biên dịch Java sử dụng nhưng điều này thực sự sẽ không cho phép bạn đạt được điều gì đó mới. Như đã chỉ ra trong ngữ cảnh, các "hướng dẫn tính năng" được đề cập này hiện đã bị xóa khỏi tập hợp các mã hợp pháp, điều này khiến chúng thậm chí còn ít hơn một tính năng.


3
Về tên phương thức, bạn có thể có nhiều hơn một <clinit>phương thức bằng cách xác định các phương thức có tên <clinit>nhưng chấp nhận tham số hoặc có kiểu không voidtrả về. Nhưng các phương thức này không hữu ích lắm, JVM sẽ bỏ qua chúng và mã byte không thể gọi chúng. Việc sử dụng duy nhất sẽ là gây nhầm lẫn cho độc giả.
Holger

2
Tôi vừa phát hiện ra rằng JVM của Oracle phát hiện một trình giám sát chưa được phát hành khi thoát phương thức và ném ra IllegalMonitorStateExceptionnếu bạn bỏ qua monitorexithướng dẫn. Và trong trường hợp một lối thoát phương thức đặc biệt không thực hiện được monitorexit, nó sẽ đặt lại màn hình một cách im lặng.
Holger

1
@Holger - không biết điều đó, tôi biết rằng ít nhất điều này có thể xảy ra trong các JVM trước đó, JRockit thậm chí còn có trình xử lý riêng cho loại triển khai này. Tôi sẽ cập nhật mục.
Rafael Winterhalter

1
Chà, đặc tả JVM không bắt buộc một hành vi như vậy. Tôi chỉ phát hiện ra nó bởi vì tôi đã cố gắng tạo ra một khóa nội tại lơ lửng bằng cách sử dụng mã byte không chuẩn như vậy.
Holger

3
Ok, tôi đã tìm thấy thông số kỹ thuật có liên quan : Khóa có cấu trúc là tình huống khi, trong khi gọi phương thức, mọi lối ra trên một màn hình đã cho khớp với một mục trước trên màn hình đó. Do không có gì đảm bảo rằng tất cả các mã được gửi tới Máy ảo Java sẽ thực hiện khóa có cấu trúc, nên việc triển khai Máy ảo Java được cho phép nhưng không bắt buộc phải thực thi cả hai quy tắc sau đảm bảo khóa có cấu trúc. Tiết chí
Holger

14

Dưới đây là một số tính năng có thể được thực hiện trong mã byte Java nhưng không phải trong mã nguồn Java:

  • Ném một ngoại lệ được kiểm tra từ một phương thức mà không tuyên bố rằng phương thức đó ném nó. Các ngoại lệ được kiểm tra và không được kiểm tra là một thứ chỉ được kiểm tra bởi trình biên dịch Java, không phải JVM. Vì điều này chẳng hạn, Scala có thể đưa ra các ngoại lệ được kiểm tra từ các phương thức mà không cần khai báo chúng. Mặc dù với các thế hệ Java, có một cách giải quyết được gọi là ném lén .

  • Có hai phương thức trong một lớp chỉ khác nhau về kiểu trả về, như đã được đề cập trong câu trả lời của Joachim : Đặc tả ngôn ngữ Java không cho phép hai phương thức trong cùng một lớp khi chúng chỉ khác nhau về kiểu trả về của chúng (nghĩa là cùng tên, cùng một danh sách đối số, ...). Tuy nhiên, đặc tả JVM không có hạn chế như vậy, vì vậy một tệp lớp có thể chứa hai phương thức như vậy, không có cách nào để tạo ra một tệp lớp như vậy bằng trình biên dịch Java thông thường. Có một ví dụ hay giải thích trong câu trả lời này .


4
Lưu ý rằng có một cách để làm điều đầu tiên trong Java. Đôi khi nó được gọi là một cú ném lén lút .
Joachim Sauer

Bây giờ thì lén lút! : D Cảm ơn đã chia sẻ.
Esko Luontola

Tôi nghĩ bạn cũng có thể sử dụng Thread.stop(Throwable)cho một cú ném lén lút. Tôi giả sử cái đã được liên kết là nhanh hơn mặc dù.
Bart van Heukelom

2
Bạn không thể tạo một cá thể mà không gọi hàm tạo trong mã byte Java. Trình xác minh sẽ từ chối bất kỳ mã nào cố gắng sử dụng một thể hiện chưa được khởi tạo. Việc thực hiện giải tuần tự hóa đối tượng sử dụng các trình trợ giúp mã gốc để tạo các cá thể mà không cần gọi hàm tạo.
Holger

Đối với một đối tượng mở rộng Foo lớp, bạn không thể khởi tạo Foo bằng cách gọi một hàm tạo được khai báo trong Object. Người xác minh sẽ từ chối nó. Bạn có thể tạo một hàm tạo như vậy bằng ReflectionFactory của Java nhưng đây không phải là một tính năng mã byte nhưng được Jni nhận ra. Câu trả lời của bạn là sai và Holger là chính xác.
Rafael Winterhalter

8
  • GOTOcó thể được sử dụng với các nhãn để tạo cấu trúc điều khiển của riêng bạn (trừ for whilevv)
  • Bạn có thể ghi đè thisbiến cục bộ bên trong một phương thức
  • Kết hợp cả hai điều này, bạn có thể tạo cuộc gọi đuôi được tối ưu hóa mã byte (Tôi thực hiện điều này trong JCompilo )

Là một điểm liên quan, bạn có thể nhận được tên tham số cho các phương thức nếu được biên dịch bằng gỡ lỗi ( Paranamer thực hiện điều này bằng cách đọc mã byte


Làm thế nào để bạn overridebiến địa phương này?
Michael

2
@Michael ghi đè là một từ quá mạnh. Ở cấp độ mã byte, tất cả các biến cục bộ được truy cập bằng một chỉ số bằng số và không có sự khác biệt giữa việc ghi vào một biến hiện có hoặc khởi tạo một biến mới (với phạm vi khác nhau), trong cả hai trường hợp, nó chỉ là ghi vào biến cục bộ. Các thisbiến có chỉ số không, nhưng ngoài việc là tiền khởi tạo với các thistài liệu tham khảo khi nhập một phương pháp dụ, nó chỉ là một biến cục bộ. Vì vậy, bạn có thể viết một giá trị khác cho nó, có thể hoạt động như thisphạm vi kết thúc hoặc thay đổi thisbiến, tùy thuộc vào cách bạn sử dụng nó.
Holger

Tôi hiểu rồi! Vì vậy, thực sự nó thiscó thể được chỉ định lại? Tôi nghĩ rằng đó chỉ là ghi đè từ mà tôi đã tự hỏi chính xác nó có nghĩa là gì.
Michael

5

Có lẽ phần 7A trong tài liệu này là mối quan tâm, mặc dù đó là về những cạm bẫy của mã byte thay vì các tính năng của mã byte .


Thú vị đọc, nhưng có vẻ như người ta không muốn (ab) sử dụng bất kỳ thứ gì trong số đó.
Bart van Heukelom

4

Trong ngôn ngữ Java, câu lệnh đầu tiên trong một hàm tạo phải là một lệnh gọi đến hàm tạo siêu lớp. Bytecode không có giới hạn này, thay vào đó, quy tắc là hàm tạo siêu lớp hoặc hàm tạo khác trong cùng lớp phải được gọi cho đối tượng trước khi truy cập các thành viên. Điều này sẽ cho phép nhiều tự do hơn như:

  • Tạo một thể hiện của một đối tượng khác, lưu trữ nó trong một biến cục bộ (hoặc ngăn xếp) và chuyển nó dưới dạng tham số cho hàm tạo siêu lớp trong khi vẫn giữ tham chiếu trong biến đó cho mục đích sử dụng khác.
  • Gọi các nhà xây dựng khác dựa trên một điều kiện. Điều này nên có thể: Làm thế nào để gọi một hàm tạo khác nhau có điều kiện trong Java?

Tôi chưa kiểm tra những thứ này, vì vậy hãy sửa cho tôi nếu tôi sai.


Bạn thậm chí có thể thiết lập các thành viên của một thể hiện trước khi gọi hàm tạo siêu lớp của nó. Tuy nhiên, không thể đọc các trường hoặc phương thức gọi trước đó.
Rafael Winterhalter

3

Một cái gì đó bạn có thể làm với mã byte, thay vì mã Java đơn giản, là tạo mã có thể được tải và chạy mà không cần trình biên dịch. Nhiều hệ thống có JRE thay vì JDK và nếu bạn muốn tạo mã động, có thể tốt hơn, nếu không dễ dàng hơn, để tạo mã byte thay vì mã Java phải được biên dịch trước khi có thể sử dụng.


6
Nhưng sau đó, bạn chỉ bỏ qua trình biên dịch, không tạo ra thứ gì đó không thể được tạo bằng trình biên dịch (nếu nó có sẵn).
Bart van Heukelom

2

Tôi đã viết một trình tối ưu hóa mã byte khi tôi là I-Play, (nó được thiết kế để giảm kích thước mã cho các ứng dụng J2ME). Một tính năng tôi đã thêm là khả năng sử dụng mã byte nội tuyến (tương tự như ngôn ngữ lắp ráp nội tuyến trong C ++). Tôi đã quản lý để giảm kích thước của hàm là một phần của phương thức thư viện bằng cách sử dụng lệnh DUP, vì tôi cần giá trị hai lần. Tôi cũng có các lệnh zero byte (nếu bạn đang gọi một phương thức lấy char và bạn muốn truyền int, mà bạn biết rằng không cần phải truyền, tôi đã thêm int2char (var) để thay thế char (var) và nó sẽ xóa hướng dẫn i2c để giảm kích thước của mã. Tôi cũng đã làm cho nó thực hiện float a = 2.3; float b = 3.4; float c = a + b; và điều đó sẽ được chuyển đổi thành điểm cố định (nhanh hơn và một số J2ME không hỗ trợ điểm nổi).


2

Trong Java, nếu bạn cố gắng ghi đè phương thức công khai bằng phương thức được bảo vệ (hoặc bất kỳ sự giảm quyền truy cập nào khác), bạn sẽ gặp lỗi: "cố gắng gán các đặc quyền truy cập yếu hơn". Nếu bạn làm điều đó với mã byte JVM, trình xác minh sẽ ổn với nó và bạn có thể gọi các phương thức này thông qua lớp cha như thể chúng là công khai.

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.