Hành vi không xác định trong Java


14

Tôi đã đọc câu hỏi này trên SO , thảo luận về một số hành vi không xác định phổ biến trong C ++ và tôi tự hỏi: Java cũng có hành vi không xác định?

Nếu đó là trường hợp, vậy một số nguyên nhân phổ biến của hành vi không xác định trong Java là gì?

Nếu không, thì các tính năng nào của Java làm cho nó không có các hành vi như vậy và tại sao các phiên bản mới nhất của C và C ++ được triển khai với các thuộc tính này?


4
Java được định nghĩa rất cứng nhắc. Kiểm tra đặc tả ngôn ngữ Java.


4
@ user1249, "hành vi không xác định" thực sự cũng được định nghĩa khá cứng nhắc.
Pacerier


Java nói gì khi bạn vi phạm "Hợp đồng"? Chẳng hạn như xảy ra khi bạn quá tải. Bằng không thể tương thích với .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/... Đó có phải là một cách thông tục undefined, nhưng không phải về mặt kỹ thuật trong cùng một cách mà C ++ là gì?
Vịt Mooing

Câu trả lời:


18

Trong Java, bạn có thể xem xét hành vi của chương trình được đồng bộ hóa không chính xác không xác định.

Java 7 JLS sử dụng từ "không xác định" một lần, trong 17.4.8. Yêu cầu thực thi và quan hệ nhân quả :

Chúng tôi sử dụng f|dđể biểu thị chức năng nhất định bằng cách hạn chế các lĩnh vực fđể d. Đối với tất cả xtrong d, f|d(x) = f(x)và cho tất cả xkhông trong d, khôngf|d(x) được xác định ...

Tài liệu API Java chỉ định một số trường hợp khi kết quả không được xác định - ví dụ: trong hàm tạo (không dùng nữa) Ngày (int year, int tháng, int day) :

Kết quả là không xác định nếu một đối số nhất định nằm ngoài giới hạn ...

Javadocs cho trạng thái ExecutorService.invoke ALL (Bộ sưu tập) :

Kết quả của phương pháp này không được xác định nếu bộ sưu tập đã cho được sửa đổi trong khi thao tác này đang diễn ra ...

Ví dụ , loại hành vi "không xác định" ít chính thức hơn có thể được tìm thấy trong ConcurrencyModificationException , trong đó các tài liệu API sử dụng thuật ngữ "nỗ lực tốt nhất":

Lưu ý rằng hành vi không nhanh không thể được đảm bảo vì nói chung, không thể thực hiện bất kỳ đảm bảo cứng nào khi có sửa đổi đồng thời không đồng bộ. Hoạt động thất bại nhanh chóng ConcurrentModificationExceptiontrên cơ sở nỗ lực tốt nhất . Do đó, sẽ là sai lầm khi viết một chương trình phụ thuộc vào ngoại lệ này vì tính chính xác của nó ...


ruột thừa

Một trong những ý kiến câu hỏi đề cập đến một bài viết của Eric Lippert cung cấp phần giới thiệu hữu ích về các vấn đề chủ đề: Hành vi được xác định theo thực hiện .

Tôi đề nghị bài viết này cho lý luận bất khả tri ngôn ngữ, mặc dù đáng lưu ý rằng tác giả nhắm mục tiêu C #, không phải Java.

Theo truyền thống, chúng tôi nói rằng một thành ngữ ngôn ngữ lập trình có hành vi không xác định nếu sử dụng thành ngữ đó có thể có bất kỳ ảnh hưởng nào; nó có thể hoạt động theo cách bạn mong đợi hoặc nó có thể xóa đĩa cứng hoặc làm hỏng máy của bạn. Hơn nữa, tác giả trình biên dịch không có nghĩa vụ cảnh báo bạn về hành vi không xác định. (Và trên thực tế, có một số ngôn ngữ trong đó các chương trình sử dụng thành ngữ "hành vi không xác định" được đặc tả ngôn ngữ cho phép để phá vỡ trình biên dịch!) ...

Ngược lại, một thành ngữ có hành vi được xác định thực hiện là hành vi trong đó tác giả trình biên dịch có một số lựa chọn về cách triển khai tính năng và phải chọn một hành vi. Như tên ngụ ý, hành vi được xác định thực hiện ít nhất là được xác định. Ví dụ, C # cho phép thực hiện ném ngoại lệ hoặc tạo giá trị khi phân chia số nguyên tràn, nhưng việc triển khai phải chọn một ngoại lệ. Nó không thể xóa đĩa cứng của bạn ...

Một số yếu tố dẫn đến một ủy ban thiết kế ngôn ngữ để lại các thành ngữ ngôn ngữ nhất định là hành vi không xác định hoặc xác định thực hiện là gì?

Yếu tố chính đầu tiên là: có hai triển khai ngôn ngữ hiện có trên thị trường không đồng ý về hành vi của một chương trình cụ thể không? ...

Yếu tố chính tiếp theo là: tính năng này có tự nhiên thể hiện nhiều khả năng khác nhau để thực hiện không, một số trong đó rõ ràng tốt hơn các tính năng khác? ...

Một yếu tố thứ ba là: tính năng này phức tạp đến mức việc phân tích chi tiết hành vi chính xác của nó sẽ khó khăn hoặc tốn kém để chỉ định? ...

Một yếu tố thứ tư là: tính năng có đặt ra gánh nặng lớn cho trình biên dịch để phân tích không? ...

Một yếu tố thứ năm là: tính năng có đặt ra gánh nặng cao cho môi trường thời gian chạy không? ...

Một yếu tố thứ sáu là: làm cho hành vi được xác định loại trừ một số tối ưu hóa chính? ...

Đó chỉ là một vài yếu tố xuất hiện trong tâm trí; tất nhiên có nhiều, rất nhiều yếu tố khác mà các ủy ban thiết kế ngôn ngữ tranh luận trước khi thực hiện một tính năng "triển khai được xác định" hoặc "không xác định".

Trên đây chỉ là một phạm vi bảo hiểm rất ngắn gọn; bài báo đầy đủ chứa các giải thích và ví dụ cho các điểm được đề cập trong đoạn trích này; nó rất đáng đọc Ví dụ, các chi tiết được đưa ra cho "yếu tố thứ sáu" có thể giúp người ta hiểu rõ hơn về động lực cho nhiều câu lệnh trong Mô hình bộ nhớ Java ( JSR 133 ), giúp hiểu tại sao một số tối ưu hóa được cho phép, dẫn đến hành vi không xác định trong khi các hành vi khác bị cấm, dẫn đến những hạn chế như xảy ra trướcyêu cầu nhân quả .

Không có tài liệu bài viết nào đặc biệt mới đối với tôi nhưng tôi sẽ bị nguyền rủa nếu tôi từng thấy nó được trình bày theo cách thanh lịch, dễ hiểu và dễ hiểu như vậy. Kinh ngạc.


Tôi sẽ thêm rằng JMM! = Phần cứng cơ bản và kết quả cuối cùng của một chương trình thực thi liên quan đến đồng thời có thể khác nhau từ việc nói WinIntel so với Solaris
Martijn Verburg

2
@MartijnVerburg đó là một điểm khá tốt. Lý do duy nhất khiến tôi ngần ngại gắn thẻ nó là "không xác định" là mô hình bộ nhớ đặt ra các ràng buộc như xảy ra trước đâyquan hệ nhân quả khi thực hiện chương trình được đồng bộ hóa chính xác
gnat

Đúng, thông số kỹ thuật xác định cách thức hoạt động theo JMM, tuy nhiên, Intel và cộng sự không luôn đồng ý ;-)
Martijn Verburg

@MartijnVerburg Tôi nghĩ rằng điểm chính của JMM là ngăn chặn tối ưu hóa rò rỉ quá mức từ các nhà sản xuất bộ xử lý "không đồng ý". Theo như tôi hiểu thì Java trước 5.0 đã làm đau đầu với DEC Alpha, khi các bài viết đầu cơ được thực hiện dưới mui xe có thể bị rò rỉ vào chương trình như "hết hơi" - do đó, yêu cầu về nhân quả đã được đưa vào JSR 133 (JMM)
gnat

9
@MartinVerburg - đây là công việc của người triển khai JVM để đảm bảo rằng JVM hoạt động theo thông số JLS / JMM trên bất kỳ nền tảng phần cứng được hỗ trợ nào. Nếu các phần cứng khác nhau hoạt động khác nhau, thì đó là công việc của người triển khai JVM để xử lý nó ... và làm cho nó hoạt động.
Stephen C

10

Ngoài đỉnh đầu, tôi không nghĩ có bất kỳ hành vi không xác định nào trong Java, ít nhất là không có ý nghĩa tương tự như trong C ++.

Lý do cho điều này là có một triết lý khác đằng sau Java so với đằng sau C ++. Mục tiêu thiết kế cốt lõi của Java là cho phép các chương trình chạy không thay đổi trên các nền tảng, do đó, đặc tả kỹ thuật xác định mọi thứ rất rõ ràng.

Ngược lại, mục tiêu thiết kế cốt lõi của C và C ++ là hiệu quả: không nên có bất kỳ tính năng nào (bao gồm tính độc lập nền tảng) mà chi phí hiệu năng ngay cả khi bạn không cần chúng. Cuối cùng, đặc tả này cố tình không xác định một số hành vi vì việc xác định chúng sẽ gây ra việc làm thêm trên một số nền tảng và do đó làm giảm hiệu suất ngay cả đối với những người viết chương trình dành riêng cho một nền tảng và nhận thức được tất cả các đặc điểm riêng của nó.

Thậm chí còn có một ví dụ về việc Java buộc phải đưa ra một cách hạn chế một dạng hành vi không xác định hạn chế vì lý do chính xác đó: từ khóarictfp được giới thiệu trong Java 1.2 để cho phép các phép tính dấu phẩy lệch khỏi chính xác theo tiêu chuẩn IEEE 754 như thông số kỹ thuật đã yêu cầu trước đây , bởi vì làm như vậy đòi hỏi phải làm thêm và làm cho tất cả các phép tính dấu phẩy động chậm hơn trên một số CPU thông thường, trong khi thực sự tạo ra kết quả tồi tệ hơn trong một số trường hợp.


2
Tôi nghĩ điều quan trọng cần lưu ý mục tiêu chính khác của Java: bảo mật và cô lập. Tôi nghĩ điều này cũng là một lý do cho việc thiếu hành vi 'không xác định' (như trong C ++).
K.Steff

3
@ K.Steff: C / C ++ siêu hiện đại hoàn toàn không phù hợp với mọi thứ liên quan đến bảo mật từ xa. Với int x=-1; foo(); x<<=1;triết lý siêu hiện đại sẽ ưu tiên viết lại foođể bất kỳ con đường nào không thoát ra đều không thể truy cập được. Điều này, nếu fooif (should_launch_missiles) { launch_missiles(); exit(1); }một trình biên dịch có thể (và theo một số người nên) đơn giản hóa nó thành đơn giản launch_missiles(); exit(1);. UB truyền thống là thực thi mã ngẫu nhiên, nhưng trước đây bị ràng buộc bởi quy luật thời gian và quan hệ nhân quả. UB cải tiến mới bị ràng buộc bởi không.
supercat

3

Java cố gắng hết sức để tiêu diệt hành vi không xác định, chính xác là do các bài học của các ngôn ngữ trước đó. Ví dụ, các biến cấp độ lớp được tự động khởi tạo; các biến cục bộ không được tự động khởi tạo vì lý do hiệu suất, nhưng có phân tích luồng dữ liệu phức tạp để ngăn chặn bất kỳ ai viết chương trình có thể phát hiện ra điều này. Tài liệu tham khảo không phải là con trỏ, vì vậy tài liệu tham khảo không hợp lệ không thể tồn tại và hội thảo nullgây ra một ngoại lệ cụ thể.

Tất nhiên vẫn còn một số hành vi không được chỉ định đầy đủ và bạn có thể viết các chương trình không đáng tin cậy nếu bạn cho rằng chúng là như vậy. Chẳng hạn, nếu bạn lặp đi lặp lại so với bình thường (không được sắp xếp) Set, ngôn ngữ đảm bảo rằng bạn sẽ thấy từng phần tử chính xác một lần, nhưng không theo thứ tự bạn sẽ thấy chúng. Thứ tự có thể giống nhau trên các lần chạy liên tiếp hoặc có thể thay đổi; hoặc nó có thể giữ nguyên miễn là không có phân bổ nào khác xảy ra, hoặc miễn là bạn không cập nhật JDK của mình, v.v ... Gần như không thể loại bỏ tất cả các hiệu ứng như vậy; chẳng hạn, bạn sẽ phải đặt hàng rõ ràng hoặc chọn ngẫu nhiên tất cả các hoạt động của Bộ sưu tập và điều đó chỉ đơn giản là không có giá trị bổ sung nhỏ không xác định.


Tài liệu tham khảo là con trỏ dưới một tên khác
tò mò

@cquilguy - "tài liệu tham khảo" thường được cho là không cho phép sử dụng thao tác số học của giá trị số của chúng, thường được phép cho "con trỏ". Do đó, cái trước là một cấu trúc an toàn hơn cái trước; kết hợp với hệ thống quản lý bộ nhớ không cho phép lưu trữ lại đối tượng trong khi tồn tại tham chiếu hợp lệ cho nó, các tham chiếu ngăn ngừa lỗi sử dụng bộ nhớ. Con trỏ không thể làm như vậy, ngay cả khi quản lý bộ nhớ thích hợp được sử dụng.
Jules

@Jules Sau đó, đây là vấn đề về thuật ngữ: bạn có thể gọi một thứ là con trỏ hoặc tham chiếu và quyết định sử dụng "tham chiếu" trong ngôn ngữ "an toàn" và "con trỏ" trong các ngôn ngữ cho phép sử dụng số học con trỏ và quản lý bộ nhớ thủ công. (AFAIK "số học con trỏ" chỉ được thực hiện trong C / C ++.)
tò mò

2

Bạn phải hiểu "Hành vi không xác định" và nguồn gốc của nó.

Hành vi không xác định có nghĩa là một hành vi không được xác định bởi các tiêu chuẩn. C / C ++ có quá nhiều triển khai trình biên dịch khác nhau và các tính năng bổ sung. Các tính năng bổ sung này gắn mã vào trình biên dịch. Điều này là do không có sự phát triển ngôn ngữ tập trung. Vì vậy, một số tính năng nâng cao từ một số trình biên dịch trở thành "hành vi không xác định".

Trong khi đó trong Java, đặc tả ngôn ngữ được kiểm soát bởi Sun-Oracle và không có ai khác cố gắng tạo ra các đặc tả và do đó không có hành vi không xác định.

Chỉnh sửa cụ thể trả lời câu hỏi

  1. Java không có các hành vi không xác định vì các tiêu chuẩn được tạo trước các trình biên dịch
  2. Các trình biên dịch C / C ++ hiện đại có nhiều / ít tiêu chuẩn hóa các triển khai, nhưng các tính năng được triển khai trước khi tiêu chuẩn hóa vẫn được gắn thẻ là "hành vi không xác định" vì ISO giữ các khía cạnh này.

2
Bạn có thể đúng rằng không có UB trong Java, nhưng ngay cả khi một thực thể kiểm soát mọi thứ, có thể có lý do để có UB, vì vậy lý do bạn đưa ra không dẫn đến kết luận.
Lập trình viên

2
Bên cạnh đó, cả C và C ++ đều được tiêu chuẩn hóa bởi ISO. Mặc dù có thể có nhiều trình biên dịch, mỗi lần chỉ có một tiêu chuẩn.
MSalters

1
@SarvexJatasra, tôi không đồng ý rằng đó là nguồn duy nhất của UB. Chẳng hạn, một UB là con trỏ lơ lửng trong hội nghị và có những lý do chính đáng để để nó là một UB trong bất kỳ ngôn ngữ nào không có GC, ngay cả khi bạn bắt đầu thông số kỹ thuật của mình ngay bây giờ. Và những lý do đó không liên quan gì đến thực tiễn hiện tại hoặc trình biên dịch hiện có.
Lập trình viên

2
@SarvexJatasra, tràn đã ký là UB vì tiêu chuẩn nói rõ ràng như vậy (thậm chí nó là ví dụ được đưa ra với định nghĩa của UB). Hủy bỏ một con trỏ không hợp lệ cũng là một UB vì lý do tương tự, tiêu chuẩn nói như vậy.
AProgrammer

2
@ bames53: Không có lợi thế nào được trích dẫn sẽ yêu cầu mức độ của trình biên dịch siêu âm vĩ độ đang thực hiện với UB. Với các trường hợp ngoại lệ của các truy cập bộ nhớ ngoài luồng và tràn ngăn xếp, có thể "tự nhiên" gây ra việc thực thi mã ngẫu nhiên, tôi không thể nghĩ ra bất kỳ tối ưu hóa hữu ích nào đòi hỏi vĩ độ rộng hơn để nói rằng hầu hết các hoạt động của UB-ish đều không xác định các giá trị (có thể hoạt động như thể chúng có "bit thừa") và chỉ có thể có hậu quả vượt quá nếu tài liệu của một triển khai rõ ràng bảo lưu quyền áp đặt như vậy; tài liệu có thể đưa ra "hành vi không ràng buộc" ...
supercat

1

Java loại bỏ về cơ bản tất cả các hành vi không xác định được tìm thấy trong C / C ++. . hiếm khi gặp phải bởi các lập trình viên.

  • Giao diện gốc Java (JNI), một cách để Java gọi mã C hoặc C ++. Có nhiều cách để làm hỏng JNI, như lấy chữ ký hàm sai, thực hiện các cuộc gọi không hợp lệ đến các dịch vụ JVM, làm hỏng bộ nhớ, phân bổ / giải phóng nội dung không chính xác và hơn thế nữa. Tôi đã mắc những lỗi này trước đây và nói chung, toàn bộ JVM gặp sự cố khi bất kỳ một luồng nào thực thi mã JNI đều phạm lỗi.

  • Thread.stop(), đó là phản đối. Trích dẫn:

    Tại sao Thread.stopbị phản đối?

    Bởi vì nó vốn không an toàn. Dừng một chuỗi khiến nó mở khóa tất cả các màn hình mà nó đã khóa. (Các màn hình được mở khóa khi ThreadDeathngoại lệ lan truyền ngăn xếp.) Nếu bất kỳ đối tượng nào được bảo vệ trước đây bởi các màn hình này ở trạng thái không nhất quán, các luồng khác có thể xem các đối tượng này ở trạng thái không nhất quán. Những đồ vật như vậy được cho là bị hư hại. Khi các luồng hoạt động trên các đối tượng bị hư hỏng, hành vi tùy ý có thể dẫn đến. Hành vi này có thể tinh tế và khó phát hiện, hoặc nó có thể được phát âm. Không giống như các ngoại lệ không được kiểm soát khác, ThreadDeathgiết chết các chủ đề một cách im lặng; do đó, người dùng không có cảnh báo rằng chương trình của mình có thể bị hỏng. Tham nhũng có thể tự biểu hiện bất cứ lúc nào sau khi thiệt hại thực tế xảy ra, thậm chí là vài giờ hoặc vài ngày trong tương lai.

    https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

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.