Since version 1.0, Java's type system has remained pretty much the same when it comes to primitive types: we have signed short (16 bits), int (32 bits), and long (64 bits), but no unsigned counterparts to these.
This is rarely a problem, but sometimes it might seem to be: especially when reading and writing binary data formats that use unsigned values.
ByteBuffer buf = ... int unsignedIntValue = buf.getInt();
Whenever the unsigned value is greater than Integer.MAX_VALUE, our variable unsignedIntValue will look like a negative number - since Java uses the common approach of representing signed values in two's complement encoding. But as long as we don't have to know the actual value, we don't have to care. Java doesn't change the bits if we don't ask it to, and writing out the number again will result in the exact same bytes as our previous input (if we make sure to use the same byte order, of course):
If we do have to understand the actual unsigned value and perhaps do math with it, we can use the next-wider data type to store it with the correct sign.
long actualUnsignedIntValue = Integer.toUnsignedLong(unsignedIntValue);
Provided we take care to stay in the range from 0 to 4294967295 (inclusive), we can now safely work with this unsigned int stored as a long.
However, we could also skip this conversion if all we need to do is blindly add or subtract values. Suppose our unsignedIntValue is some kind of counter that we need to increment before writing it back, then we can just do that as usual:
It'll just work, thanks to the magic of two's complement. Not all operations are safe, however. Comparison and division, for instance, require special care. Fortunately, the Java standard library introduced specific methods to handle these, such as compareUnsigned().
But if we've used the "unsigned int in long" approach, how do we write it back as an unsigned four-byte int? After all, a long requires 8 bytes instead of 4? Well, just cast it back to an int of course:
This kind of typecast is called a narrowing primitive conversion and it's really simple: it cuts off all the highest-order bits that don't fit into the target type. In this case, it simply cuts off the 4 highest-order bytes of the long and puts the remaining 4 bytes into the int. That's it. There is no bounds checking or any other inspection or conversion of the raw bits.
Same approach as above, just use int as the bigger "container" for unsigned short values if need be.
This is where it gets a little tricky, as we've reached the upper end of the primitive types in Java: you'd have to use BigInteger if you want a bigger container type to represent all values directly.