Hoàn toàn không có vấn đề trong việc tạo các phiên bản của một lớp trong chính lớp đó. Vấn đề gà hoặc trứng rõ ràng được giải quyết theo nhiều cách khác nhau trong khi chương trình đang được biên dịch và khi chương trình đang được chạy.
Compile-time
Khi một lớp mà tạo ra một thể hiện của bản thân đã được biên soạn, trình biên dịch thấy rằng lớp có một circular dependency trên chính nó. Sự phụ thuộc này rất dễ giải quyết: trình biên dịch biết rằng lớp đó đã được biên dịch sao cho nó sẽ không cố gắng biên dịch lại nó. Thay vào đó, nó giả vờ rằng lớp đã tồn tại tạo ra mã cho phù hợp.
Run-time
Các vấn đề con gà hay quả trứng lớn nhất với một lớp tạo một đối tượng của chính nó là khi lớp thậm chí không tồn tại được nêu; có nghĩa là, khi lớp đang được nạp. Vấn đề này được giải quyết bằng cách phá vỡ lớp tải thành hai bước: đầu tiên lớp là được xác định và sau đó nó được khởi tạo được khởi tạo.
Xác định phương tiện đăng ký lớp với hệ thống thời gian chạy (JVM hoặc CLR), để biết cấu trúc mà đối tượng của lớp có và mã nào sẽ được chạy khi hàm tạo và phương thức của nó được gọi.
Sau khi lớp đã được xác định, nó được khởi tạo. Điều này được thực hiện bằng cách khởi tạo các thành viên tĩnh và chạy các khối khởi tạo tĩnh và các thứ khác được định nghĩa trong ngôn ngữ cụ thể. Nhớ lại rằng lớp đã được định nghĩa tại thời điểm này, do đó, thời gian chạy biết các đối tượng của lớp trông như thế nào và mã nào sẽ được chạy để tạo chúng. Điều này có nghĩa là không có vấn đề gì để tạo ra các đối tượng của lớp khi khởi tạo nó.
Dưới đây là một ví dụ minh họa cách khởi tạo lớp và instantiation tương tác trong Java:
class Test {
static Test instance = new Test();
static int x = 1;
public Test() {
System.out.printf("x=%d\n", x);
}
public static void main(String[] args) {
Test t = new Test();
}
}
Hãy bước qua bao JVM sẽ chạy chương trình này. Đầu tiên JVM tải lớp Test
.Điều này có nghĩa rằng lớp là lần đầu tiên định nghĩa, do đó JVM biết rằng
- một lớp được gọi là
Test
tồn tại và rằng nó có một phương pháp main
và một constructor, và rằng
- lớp
Test
có hai tĩnh các biến số, được gọi là x
và một biến khác được gọi là instance
và
- bố cục đối tượng của lớp
Test
là gì. Nói cách khác: đối tượng trông như thế nào; những gì thuộc tính nó có. Trong trường hợp này, Test
không có bất kỳ thuộc tính cá thể nào.
Bây giờ lớp được xác định, nó là được khởi tạo. Trước hết, giá trị mặc định 0
hoặc null
được gán cho mọi thuộc tính tĩnh. Điều này đặt x
thành 0
. Sau đó, JVM thực thi các khởi tạo trường tĩnh trong thứ tự mã nguồn. Có hai:
- Tạo một thể hiện của lớp
Test
và gán cho instance
. Có hai bước để tạo ví dụ:
- Bộ nhớ đầu tiên được phân bổ cho đối tượng. JVM có thể làm điều này vì nó đã biết bố cục đối tượng từ giai đoạn định nghĩa lớp.
- Nhà xây dựng
Test()
được gọi để khởi tạo đối tượng. JVM có thể làm điều này vì nó đã có mã cho hàm tạo từ giai đoạn định nghĩa lớp. Hàm khởi tạo in ra giá trị hiện tại của x
, là 0
.
- Đặt biến tĩnh
x
to 1
.
Chỉ bây giờ lớp đã tải xong. Lưu ý rằng JVM đã tạo ra một cá thể của lớp, mặc dù nó chưa được nạp đầy đủ. Bạn có bằng chứng về thực tế này vì hàm tạo đã in giá trị mặc định ban đầu 0
cho x
.
Bây giờ JVM đã tải lớp này, nó gọi phương thức main
để chạy chương trình. Phương thức main
tạo một đối tượng khác của lớp Test
- thứ hai trong quá trình thực hiện chương trình. Một lần nữa, hàm tạo sẽ in ra giá trị hiện tại của x
, hiện là 1
. Sản lượng đầy đủ của chương trình là:
x=0
x=1
Như bạn có thể thấy không có vấn đề con gà hay quả trứng: việc tách lớp tải vào định nghĩa và khởi tạo giai đoạn tránh những vấn đề hoàn toàn.
Điều gì xảy ra khi một cá thể của đối tượng muốn tạo một cá thể khác, như trong mã bên dưới?
class Test {
Test buggy = new Test();
}
Khi bạn tạo đối tượng của lớp này, một lần nữa không có vấn đề cố hữu. JVM biết cách đối tượng nên được đặt ra trong bộ nhớ để nó có thể cấp phát bộ nhớ cho nó. Nó đặt tất cả các thuộc tính về giá trị mặc định của chúng, vì vậy buggy
được đặt thành null
. Sau đó, JVM bắt đầu khởi tạo đối tượng.Để làm điều này, nó phải tạo một đối tượng khác của lớp Test
. Giống như trước đây, JVM đã biết cách làm điều đó: nó phân bổ bộ nhớ, đặt thuộc tính là null
và bắt đầu khởi tạo đối tượng mới ... có nghĩa là nó phải tạo đối tượng thứ ba của cùng một lớp, và sau đó là thứ tư, thứ năm, v.v. cho đến khi nó hết dung lượng bộ nhớ hoặc bộ nhớ heap.
Không có vấn đề khái niệm nào ở đây bạn nghĩ: đây chỉ là trường hợp phổ biến của một đệ quy vô hạn trong một chương trình được viết kém. Các đệ quy có thể được kiểm soát ví dụ bằng cách sử dụng một truy cập; các nhà xây dựng của lớp này sử dụng đệ quy để thực hiện một chuỗi các đối tượng:
class Chain {
Chain link = null;
public Chain(int length) {
if (length > 1) link = new Chain(length-1);
}
}
Nếu bạn không tạo ra 'MyClass 'trong' MyClass' 'constructor (Yay cho đệ quy vô hạn) hoàn toàn không có vấn đề gì khi làm điều đó. Mẫu thiết kế tổng hợp thậm chí còn dựa trên đó. Tôi thực sự không hiểu câu hỏi là gì. Ngoài ra, 'public void class' sẽ không bao giờ biên dịch. –
Tôi biết rằng nó có thể được thực hiện nhưng câu hỏi của tôi là: Không phải là như bạn đang sử dụng chức năng mà không tạo ra các chức năng ở nơi đầu tiên. Làm thế nào bạn có thể giải thích điều này cho ai đó chỉ biết lập trình chức năng? –
Phần nào của quá trình tải và khởi tạo lớp vẫn chưa rõ ràng? – Joni