Làm cách nào để kiểm tra xem việc nhân hai số trong Java có gây ra lỗi tràn không?


101

Tôi muốn xử lý trường hợp đặc biệt khi nhân hai số với nhau gây ra tràn. Mã trông giống như sau:

int a = 20;
long b = 30;

// if a or b are big enough, this result will silently overflow
long c = a * b;

Đó là một phiên bản đơn giản hóa. Trong chương trình thực abcó nguồn ở nơi khác trong thời gian chạy. Những gì tôi muốn đạt được là một cái gì đó như thế này:

long c;
if (a * b will overflow) {
    c = Long.MAX_VALUE;
} else {
    c = a * b;
}

Làm thế nào để bạn đề nghị tôi viết mã này tốt nhất?

Cập nhật: abluôn không tiêu cực trong kịch bản của tôi.


6
Thật tệ khi Java không cung cấp quyền truy cập gián tiếp vào cờ tràn của CPU , như đã làm trong C # .
Drew Noakes

Câu trả lời:


92

Java 8 có Math.multiplyExact, Math.addExactv.v. cho int và dài. Những thứ này tạo ra một ArithmeticExceptiontràn không được kiểm soát .


59

Nếu abcả hai đều tích cực thì bạn có thể sử dụng:

if (a != 0 && b > Long.MAX_VALUE / a) {
    // Overflow
}

Nếu bạn cần xử lý cả số dương và số âm thì phức tạp hơn:

long maximum = Long.signum(a) == Long.signum(b) ? Long.MAX_VALUE : Long.MIN_VALUE;

if (a != 0 && (b > 0 && b > maximum / a ||
               b < 0 && b < maximum / a))
{
    // Overflow
}

Đây là một bảng nhỏ mà tôi đã thu thập để kiểm tra điều này, giả sử rằng tràn xảy ra ở -10 hoặc +10:

a =  5   b =  2     2 >  10 /  5
a =  2   b =  5     5 >  10 /  2
a = -5   b =  2     2 > -10 / -5
a = -2   b =  5     5 > -10 / -2
a =  5   b = -2    -2 < -10 /  5
a =  2   b = -5    -5 < -10 /  2
a = -5   b = -2    -2 <  10 / -5
a = -2   b = -5    -5 <  10 / -2

Tôi nên đề cập rằng a và b luôn không âm trong kịch bản của tôi, điều này sẽ đơn giản hóa phần nào cách tiếp cận này.
Steve McLeod

3
Tôi nghĩ rằng đây có thể thất bại một trường hợp: a = -1, b = 10. Tối đa / một kết quả biểu hiện trong Integer.MIN_VALUE và phát hiện tràn bộ nhớ khi không tồn tại
Kyle

Điều này thực sự tốt đẹp. Đối với những người tự hỏi, lý do điều này hoạt động là đối với số nguyên n, n > xgiống như n > floor(x). Đối với phép chia số nguyên dương thực hiện một tầng ngầm định. (Đối với số âm, nó làm tròn lên thay thế)
Thomas Ahle

Để giải quyết vấn đề a = -1và sự cố b = 10, hãy xem câu trả lời của tôi bên dưới.
Jim Pivarski

17

Có các thư viện Java cung cấp các phép toán số học an toàn, giúp kiểm tra dòng tràn / dòng dưới dài. Ví dụ: LongMath.checkedMultiply của Guava (long a, long b) trả về tích của ab, miễn là nó không bị tràn và ném ArithmeticExceptionnếu a * btràn trong longsố học có dấu.


4
Đây là câu trả lời tốt nhất - sử dụng thư viện được triển khai bởi những người thực sự hiểu về số học máy trong Java và đã được rất nhiều người thử nghiệm. Đừng cố gắng viết của riêng bạn hoặc sử dụng bất kỳ đoạn mã chưa được kiểm tra nửa vời nào được đăng trong các câu trả lời khác!
Giàu có

@Enerccio - Tôi không hiểu nhận xét của bạn. Bạn đang nói rằng Guava sẽ không hoạt động trên tất cả các hệ thống? Tôi có thể đảm bảo với bạn rằng nó sẽ hoạt động ở mọi nơi mà Java làm được. Bạn có nói rằng việc sử dụng lại mã nói chung là một ý tưởng tồi? Tôi không đồng ý nếu vậy.
Giàu

2
@Rich Tôi đang nói bao gồm thư viện khổng lồ để bạn có thể sử dụng một chức năng là một ý tưởng tồi.
Enerccio

Tại sao? Nếu bạn đang viết một ứng dụng lớn, chẳng hạn như cho một doanh nghiệp, thì một JAR bổ sung trên classpath sẽ không gây hại gì và Guava có rất nhiều mã rất hữu ích trong đó. Sẽ tốt hơn nhiều nếu sử dụng lại mã đã được kiểm tra cẩn thận của họ hơn là cố gắng viết phiên bản tương tự của riêng bạn (mà tôi cho rằng đó là những gì bạn đề xuất?). Nếu bạn đang viết trong một môi trường mà một JAR bổ sung sẽ rất đắt (ở đâu? Nhúng Java?) Thì có lẽ bạn chỉ nên giải nén lớp này từ Guava. Sao chép một câu trả lời chưa được kiểm tra từ StackOverflow có tốt hơn sao chép mã đã được kiểm tra cẩn thận của Guava không?
Giàu

Việc ném một ngoại lệ có hơi quá mức cần thiết cho một thứ gì đó mà một điều kiện nếu-thì có thể xử lý?
cookingtim

6

Bạn có thể sử dụng java.math.BigInteger để thay thế và kiểm tra kích thước của kết quả (chưa kiểm tra mã):

BigInteger bigC = BigInteger.valueOf(a) * multiply(BigInteger.valueOf(b));
if(bigC.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
  c = Long.MAX_VALUE;
} else {
  c = bigC.longValue()
}

7
tôi thấy giải pháp này khá chậm
nothrow

Đó có lẽ là cách tốt nhất để làm điều đó. Tôi cho rằng đây là một ứng dụng số, đó là lý do tại sao tôi không đề xuất nó một cách trực tiếp, nhưng đây thực sự có lẽ là cách tốt nhất để giải quyết vấn đề này.
Stefan Kendall

2
Tôi không chắc bạn có thể sử dụng toán tử '>' với BigInteger. phương thức CompareTo nên được sử dụng.
Pierre

được thay đổi thành CompareTo và tốc độ có thể quan trọng hoặc có thể không, tùy thuộc vào trường hợp mã sẽ được sử dụng.
Ulf Lindback

5

Sử dụng logarit để kiểm tra kích thước của kết quả.


ý bạn là ceil(log(a)) + ceil(log(b)) > log(Long.MAX):?
Thomas Jung

1
Tôi đã kiểm tra nó. Đối với các giá trị nhỏ, tốc độ này nhanh hơn 20% so với BigInteger và đối với các giá trị gần TỐI ĐA, tốc độ này cũng gần như nhau (nhanh hơn 5%). Mã của Yossarian là nhanh nhất (nhanh hơn 95% & 75% so với BigInteger).
Thomas Jung

Tôi nghi ngờ rằng nó có thể thất bại trong một số trường hợp.
Tom Hawtin - tackline

Hãy nhớ rằng một bản ghi số nguyên chỉ đếm số lượng các số 0 đứng đầu và bạn có thể tối ưu hóa một số trường hợp phổ biến (ví dụ: if ((a | b) & 0xffffffffff00000000L) == 0) bạn biết mình an toàn). Mặt khác, trừ khi bạn có thể tối ưu hóa của mình xuống 30/40 chu kỳ đồng hồ cho các trường hợp phổ biến nhất, phương pháp của John Kugelman có thể sẽ hoạt động tốt hơn (phép chia số nguyên là c. 2 bit / chu kỳ đồng hồ như tôi nhớ lại).
Neil Coffey

PS Sorr, tôi nghĩ rằng tôi cần một bộ thêm chút trong AND mặt nạ (0xffffffff80000000L) - đó là hơi muộn, nhưng bạn sẽ có được ý tưởng ...
Neil Coffey

4

Java có một cái gì đó giống như int.MaxValue? Nếu có, thì hãy thử

if (b != 0 && Math.abs(a) > Math.abs(Long.MAX_VALUE / b))
{
 // it will overflow
}

chỉnh sửa: đã thấy Long.MAX_VALUE được đề cập


Tôi đã không phản đối nhưng Math.Abs(a)không hoạt động nếu aLong.MIN_VALUE.
John Kugelman

@John - a và b là> 0. Tôi nghĩ cách tiếp cận của Yossarian (b! = 0 && a> Long.MAX_VALUE / b) là tốt nhất.
Thomas Jung

@Thomas, a và b là> = 0, nghĩa là, không âm.
Steve McLeod

2
Đúng, nhưng trong trường hợp đó thì không cần Abs's. Nếu số âm được cho phép thì điều này không thành công đối với ít nhất một trường hợp cạnh. Đó là tất cả những gì tôi đang nói, chỉ là tinh thần.
John Kugelman

trong Java, bạn phải sử dụng Math.abs, không phải Math.Abs ​​(anh chàng C #?)
dfa

4

Đây là cách đơn giản nhất tôi có thể nghĩ ra

int a = 20;
long b = 30;
long c = a * b;

if(c / b == a) {
   // Everything fine.....no overflow
} else {
   // Overflow case, because in case of overflow "c/b" can't equal "a"
}

3

Bị đánh cắp từ jruby

    long result = a * b;
    if (a != 0 && result / a != b) {
       // overflow
    }

CẬP NHẬT: Mã này ngắn và hoạt động tốt; tuy nhiên, nó không thành công cho a = -1, b = Long.MIN_VALUE.

Một cải tiến có thể có:

long result = a * b;
if( (Math.signum(a) * Math.signum(b) != Math.signum(result)) || 
    (a != 0L && result / a != b)) {
    // overflow
}

Lưu ý rằng điều này sẽ bắt một số tràn mà không có bất kỳ sự phân chia nào.


Bạn có thể sử dụng Long.signum thay vì Math.signum
aditsu thoát ra vì SE là BUỔI TỐI,

3

Như đã được chỉ ra, Java 8 có các phương thức Math.xxxExact để loại bỏ các ngoại lệ khi tràn.

Nếu bạn không sử dụng Java 8 cho dự án của mình, bạn vẫn có thể "mượn" các triển khai của chúng khá nhỏ gọn.

Dưới đây là một số liên kết đến các triển khai này trong kho lưu trữ mã nguồn JDK, không có gì đảm bảo liệu chúng sẽ vẫn hợp lệ nhưng trong mọi trường hợp, bạn có thể tải xuống nguồn JDK và xem cách chúng thực hiện phép thuật bên trong java.lang.Mathlớp.

Math.multiplyExact(long, long) http://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/java.base/share/classes/java/lang/Math.java#l925

Math.addExact(long, long) http://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/java.base/share/classes/java/lang/Math.java#l830

Vân vân.

CẬP NHẬT: loại bỏ các liên kết không hợp lệ đến trang web của bên thứ 3 thành các liên kết đến các kho lưu trữ Mercurial của Open JDK.


2

Tôi không chắc tại sao không ai xem xét giải pháp như:

if (Long.MAX_VALUE/a > b) {
     // overflows
} 

Chọn một lớn hơn trong hai số.


2
Tôi không nghĩ nó là vấn đề nếu alà lớn hơn hay nhỏ hơn?
Thomas Ahle

2

Tôi muốn xây dựng câu trả lời của John Kugelman mà không thay thế nó bằng cách chỉnh sửa trực tiếp. Nó hoạt động cho trường hợp thử nghiệm của anh ấy ( MIN_VALUE = -10, MAX_VALUE = 10) vì tính đối xứng của MIN_VALUE == -MAX_VALUE, điều này không đúng với trường hợp số nguyên bù của hai. Trong thực tế MIN_VALUE == -MAX_VALUE - 1,.

scala> (java.lang.Integer.MIN_VALUE, java.lang.Integer.MAX_VALUE)
res0: (Int, Int) = (-2147483648,2147483647)

scala> (java.lang.Long.MIN_VALUE, java.lang.Long.MAX_VALUE)
res1: (Long, Long) = (-9223372036854775808,9223372036854775807)

Khi được áp dụng cho true MIN_VALUEMAX_VALUE, câu trả lời của John Kugelman tạo ra trường hợp tràn khi a == -1b ==bất kỳ điều gì khác (điểm đầu tiên được Kyle nêu ra). Đây là một cách để khắc phục nó:

long maximum = Long.signum(a) == Long.signum(b) ? Long.MAX_VALUE : Long.MIN_VALUE;

if ((a == -1 && b == Long.MIN_VALUE) ||
    (a != -1 && a != 0 && ((b > 0 && b > maximum / a) ||
                           (b < 0 && b < maximum / a))))
{
    // Overflow
}

Nó không phải là một giải pháp chung cho bất kỳ MIN_VALUEMAX_VALUE, nhưng nó là chung cho Java LongIntegervà bất kỳ giá trị ab.


Tôi chỉ nghĩ rằng nó sẽ phức tạp một cách không cần thiết, vì giải pháp này chỉ hoạt động nếu MIN_VALUE = -MAX_VALUE - 1, không phải bất kỳ trường hợp nào khác (bao gồm cả trường hợp thử nghiệm ví dụ của bạn). Tôi sẽ phải thay đổi rất nhiều.
Jim Pivarski

1
Vì những lý do ngoài nhu cầu của người đăng ban đầu; cho những người như tôi đã tìm thấy trang này vì họ cần một giải pháp cho một trường hợp tổng quát hơn (xử lý các số âm chứ không phải Java 8 hoàn toàn). Trên thực tế, vì giải pháp này không liên quan đến bất kỳ chức năng nào ngoài số học và logic thuần túy, nên nó cũng có thể được sử dụng cho C hoặc các ngôn ngữ khác.
Jim Pivarski

1

Có lẽ:

if(b!= 0 && a * b / b != a) //overflow

Không chắc chắn về "giải pháp" này.

Chỉnh sửa: Đã thêm b! = 0.

Trước khi bạn phản đối : a * b / b sẽ không được tối ưu hóa. Đây sẽ là lỗi trình biên dịch. Tôi vẫn chưa thấy trường hợp nào có thể che được lỗi tràn.


Cũng không thành công khi tràn gây ra một vòng lặp hoàn hảo.
Stefan Kendall

Bạn có một ví dụ về những gì bạn muốn nói?
Thomas Jung

Chỉ cần viết một thử nghiệm nhỏ: Sử dụng BigInteger chậm hơn 6 lần so với sử dụng cách tiếp cận phân chia này. Vì vậy, tôi cho rằng việc kiểm tra bổ sung cho các trường hợp góc là đáng giá về hiệu suất.
mhaller

Không biết nhiều về trình biên dịch java, nhưng một biểu thức như a * b / bcó thể được tối ưu hóa cho phù hợp với anhiều ngữ cảnh khác.
SingleNegationElimination

TokenMacGuy - nó không thể được tối ưu hóa như vậy nếu có nguy cơ bị tràn.
Tom Hawtin - tackline

1

Có lẽ điều này sẽ giúp bạn:

/**
 * @throws ArithmeticException on integer overflow
 */
static long multiply(long a, long b) {
    double c = (double) a * b;
    long d = a * b;

    if ((long) c != d) {
        throw new ArithmeticException("int overflow");
    } else {
        return d;
    }
}

Sẽ không trợ giúp như một trong những toán hạng được long.
Tom Hawtin - tackline

1
Bạn thậm chí đã kiểm tra điều này? Đối với bất kỳ giá trị lớn nào nhưng không tràn của a & b, điều này sẽ không thành công do lỗi làm tròn trong phiên bản kép của phép nhân (thử ví dụ: 123456789123L và 74709314L). Nếu bạn không hiểu số học máy móc, việc đoán câu trả lời cho loại câu hỏi chính xác này còn tệ hơn là không trả lời, vì nó sẽ đánh lừa mọi người.
Giàu có

-1

c / c ++ (long * long):

const int64_ w = (int64_) a * (int64_) b;    
if ((long) (w >> sizeof(long) * 8) != (long) w >> (sizeof(long) * 8 - 1))
    // overflow

java (int * int, xin lỗi tôi không tìm thấy int64 trong java):

const long w = (long) a * (long) b;    
int bits = 32; // int is 32bits in java    
if ( (int) (w >> bits) != (int) (w >> (bits - 1))) {
   // overflow
}

1. lưu kết quả ở kiểu lớn (int * int đặt kết quả thành long, long * long đưa vào int64)

2.cmp kết quả >> bit và kết quả >> (bit - 1)

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.