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.Unsafe
vẫ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à .
, ;
, [
và /
. Ngoài ra, các phương thức không được đặt tên <init>
hoặc <clinit>
không thể chứa <
và >
.
Xác định lại final
các tham số hoặc this
tham chiếu
final
cá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 this
tham 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 this
tham chiếu tại chỉ mục 0
trong một khung phương thức duy nhất.
Tái chỉ định final
cá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 final
cá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 void
hoặ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#baz
trong 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_SUPER
cờ 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 RuntimeException
khi foo
được gọi trong một thể hiện của Bar
. Không thể định nghĩa Foo::foo
phương thức để gọi phương thức của chính bar
nó được định nghĩa trong Foo
. Là bar
mộ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 INVOKESPECIAL
opcode liên kết trực tiếp bar
lời gọi phương thức Foo::foo
với Foo
phiê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 @Target
mà 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 @Target
chú 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
, char
và boolean
cá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
, long
và double
cá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 synchronized
khố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 IllegalMonitorStateException
việ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 return
câ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 synchronized
khố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:
- 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).
- 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
B
mở rộng giao diện A
như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 Bar
thể 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 String
giá 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 parameter
cờ đượ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 JSR
và RET
. 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êmNOOP
Hướ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.