Over a million developers have joined DZone.

Inconsistent Operation Widen Rules in Java

DZone's Guide to

Inconsistent Operation Widen Rules in Java

· Java Zone
Free Resource

What every Java engineer should know about microservices: Reactive Microservices Architecture.  Brought to you in partnership with Lightbend.

When you perform a unary or binary operation in Java the standard behaviour is to use the widest operand (or a wider one for byte, short and char).  This is simple to understand but can be confusing if you consider what the optimal type is likely to be.


When you perform multiplication, you often get a much large number than either of the individual numbers in magnitude. i.e. |a*b| >> |a| and |a*b| >> |b| is often the case.  And for small types this works as expected

Consider this program

public static void main(String[] args) throws IOException {
    System.out.println(is(Byte.MAX_VALUE * Byte.MAX_VALUE));
    System.out.println(is(Short.MAX_VALUE * Short.MAX_VALUE));
    System.out.println(is(Character.MAX_VALUE * Character.MAX_VALUE));
    System.out.println(is(Integer.MAX_VALUE * Integer.MAX_VALUE));
    System.out.println(is(Long.MAX_VALUE * Long.MAX_VALUE));
static String is(byte b) {
    return "byte: " + b;
static String is(char ch) {
    return "char: " + ch;
static String is(short i) {
    return "short: " + i;
static String is(int i) {
    return "int: " + i;
static String is(long l) {
    return "long: " + l;
which prints
int: 16129
int: 1073676289
int: -131071
int: 1
long: 1

Only byte * byte and short * short doesn't overflow as these have been widened.  char * char isn't a meaningful operation even though it is allowed. But int * int does overflow even though we have a long type which could store this value without an overflow.  Both byte and short are widened implicitly but not int. longshould really be widened but we don't have a wider primitive type, which would have made sense once upon a time however a 64-bit primitive doesn't seem so long these days.


Division is a little strange in the sense that the divisor can widen the result. Having a wider divisor than the numerator doesn't mean the result will be bigger (but is usually smaller)
System.out.println(is(Byte.MAX_VALUE / (byte) 1));
System.out.println(is(Byte.MAX_VALUE / (short) 1));
System.out.println(is(Byte.MAX_VALUE / (char) 1));
System.out.println(is(Byte.MAX_VALUE / (int) 1));
System.out.println(is(Byte.MAX_VALUE/ (long) 1));


int: 127
int: 127
int: 127
int: 127
long: 127
When you divide a byte/byte you get an int even though you can't get a value larger than a byte. (unless you divide Byte.MIN_VALUE by -1 in which case a short would do) and if you divide a byte/long you get a long even though the value still cannot be bigger than a byte.


When you perform modulus a % b, the result cannot be bigger than b.  And yet modulus will wider a result rather than reduce it.
System.out.println(is(Byte.MAX_VALUE % Byte.MAX_VALUE));
System.out.println(is(Byte.MAX_VALUE % Short.MAX_VALUE));
System.out.println(is(Byte.MAX_VALUE % Character.MAX_VALUE));
System.out.println(is(Byte.MAX_VALUE % Integer.MAX_VALUE));
System.out.println(is(Byte.MAX_VALUE % Long.MAX_VALUE));
System.out.println(is(Byte.MAX_VALUE % (byte) 2));
System.out.println(is(Short.MAX_VALUE % (byte) 2));
System.out.println(is(Character.MAX_VALUE % (byte) 2));
System.out.println(is(Integer.MAX_VALUE % (byte) 2));
System.out.println(is(Long.MAX_VALUE % (byte) 2));

int: 0
int: 127
int: 127
int: 127
long: 127
int: 1
int: 1
int: 1
int: 1
long: 1
If you modulus X by a number the result can't get any wider/bigger than X, it can only get smaller.  However, the JLS say it must get wider. If you modulus X by a byte, the result can only ever be in the range of a byte.


I also mentioned unary operations, and perhaps the simplest is unary minus.
int: 128
int: 32768
int: 0
int: -2147483648
long: -9223372036854775808
In the first three cases, the type is widened.  A byte could be widened to a short, but it is correct as an int.  However for int and long, it is not widened and you can get a rare overflow.
A little odder is unary plus which doesn't change the value (and thus cannot change it's range) but can widen the value.
int: -128
int: -32768
int: 0
int: -2147483648
long: -9223372036854775808

Can we fix this?

Unfortunately not.  There is too much code which depends on this logic. e.g. say you write something like this.
long i = ...
byte b = ...
long l = i % b + Integer.MAX_VALUE;
If i % b was to turn from a long into a byte, this expression could overflow.


Java may widen some values when you need it to, but also not widen some int operations which really should be a long.  It will never give a narrower result, even if this could be more logical.
What we need to do is to be aware of the edge cases, in particular int * int, and know to widen them ourselves when we see such an operation. e.g.
long l = (long) a * b;
Unless we are confident a * b will fit in an int value. 

Microservices for Java, explained. Revitalize your legacy systems (and your career) with Reactive Microservices Architecture, a free O'Reilly book. Brought to you in partnership with Lightbend.


Published at DZone with permission of Peter Lawrey, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.


Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.


{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}