1. Typecasting

Typecasting in Java

Variables of primitive types (with the exception of the boolean type) are used to store various types of numbers. Although the types of variables never changed, there is a place where you can convert from one type to another. And that place is assignment.

Variables of different types can be assigned to each other. When you do this the value of a variable of one type is converted to a value of another type and assigned to the second variable. In this regard, we can identify two kinds of type conversion: widening and narrowing.

Widening is like to moving a value from a small basket to a large one: this operation is seamless and painless. Narrowing happens when you move a value from a large basket to a small one: there may not be enough space, and you will have to throw something away.

Here are the types, sorted by basket size:

Typecasting in Java 2


2. Widening type conversions

It's often necessary to assign a variable of one numeric type to a variable of another numeric type. How do you do that?

Java has 4 integer types:

Type Size
byte 1 byte
short 2 bytes
int 4 bytes
long 8 bytes

Variable stored in smaller baskets can always be assigned to variables stored in larger baskets.

int, short and byte variables can be easily assigned to long variables. short and byte variables can be assigned to int variables. And byte variables can be assigned to short variables.

Examples:

Code Description
byte a = 5;
short b = a;
int c = a + b;
long d = c * c;
This code will compile just fine.

Such a conversion, from a smaller to a larger type, is called a widening type conversion.

What about real numbers?

With them, everything is the same — size matters:

Type Size
float 4 bytes
double 8 bytes

float variables can be assigned to double variables without any problems. But things are more interesting with the integer types.

You can assigned any integer variable to a float variable. Even the long type, which is 8 bytes long. And you can assign whatever you want — any integer variable or float variable — to a double variable:

Code Note
long a = 1234567890;
float b = a;
double c = a;

b == 1.23456794E9
c == 1.23456789E9

Note that converting to a real type may result in loss of precision due to the lack of sufficient significant digits.

When converting from integers to floating-point numbers, the lower order parts of numbers may be discarded. But since fractional numbers are understood to store approximate values, such assignment operations are permitted.


3. Narrowing type conversions

What about the other possibilities? What if you need to assign a long value to an int variable?

Imagine a variable as a basket. We have baskets of various sizes: 1, 2, 4 and 8 bytes. It's not a problem to transfer apples from a smaller basket to a larger one. But when shifting from a larger basket to a smaller one, some of the apples may be lost.

This transformation — from a larger type to a smaller type — is called a narrowing type conversion. When performing an assignment operation like this, part of a number may simply not fit into the new variable and may therefore be discarded.

When narrowing a type, we must explicitly tell the compiler that we are not making a mistake, that we are deliberately discarding part of the number. The typecast operator is used for this. It is a type name in parentheses.

In such situations, the Java compiler requires the programmer to specify the typecast operator. In general, it looks like this:

(type) expression

Examples:

Code Description
long a = 1;
int b = (int) a;
short c = (short) b;
byte d = (byte) c;
Each time the typecast operator must be indicated explicitly

Here a is equal to 1, and perhaps the typecast operator seems like overkill. But what if a were bigger?

Code Description
long a = 1000000;
int b = (int) a;
short c = (short) b;
byte d = (byte) c;
a == 1000000
b == 1000000
c == 16960
d == 64

One million fits perfectly into a long and into an int. But when assigning one million to a short variable, the first two bytes are discarded, and only the last two bytes are retained. And when assigning to a byte, the only thing that remains is the last byte.

How the numbers are arranged in memory:

Type Binary notation Decimal notation
int 0b00000000000011110100001001000000 1000000
short 0b0100001001000000 16.960
byte 0b01000000 64

char type

A char, like a short, occupies two bytes, but to convert one to another, you always need to use a typecast operator. The issue here is that the short type is signed and can contain values from -32,768 to +32,767, but the char type is unsigned and can contain values from 0 to 65,535.

Negative numbers cannot be stored in a char, but they can be stored in a short. And a short cannot store numbers larger than 32,767, but such numbers can be stored in a char.


4. Type of an expression

What if variables of different types are used in the same expression? Logically, we understand that they first need to be converted to a common type. But which one?

To the larger one, of course.

Java always converts to the larger type. Roughly speaking, one of the type is first widened and only then is the operation performed using values of the same type.

If an int and a long are involved in an expression, the value of the int will be converted to a long and only then will the operation proceed:

Code Description
int a = 1;
long b = 2;
long c = a + b;
a will be widened to a long and then the addition will occur.

Floating-point numbers

If an integer and a floating-point number (float or double) are involved in an expression, the integer will be converted to a floating-point number (float or double), and only then will the operation be performed.

If the operation involves a float and a double, then the float will be converted to a double. Which is actually expected.

Surprise

The byte, short, and char types are always converted to int when interacting with each other. There's a good reason why the int type is considered the standard integer type.

If you multiply a byte by a short, you get an int. If you multiply a byte by a byte, you get an int. Even if you add a byte and a byte, you get an int.

There are several reasons for this. Examples:

Code Description
byte a = 110;
byte b = 120;
byte c = a * b;  // Error
110 * 120 is 13,200, which is slightly greater than the maximum value of the byte type: 127
byte a = 110;
byte b = 120;
byte c = a + b; // Error
110 + 120 is 230, which is also slightly greater than the maximum value of the byte type: 127

In general, when multiplying an 8-bit (1 byte) number by an 8-bit (1 byte) number, we get a number that occupies 16-bits bits (2 bytes)

As a result, all operations with integer types that are smaller than int are always immediately converted to ints. And that means that if you want to store the result of the calculation in a variable of a type that is smaller than an int, then you will always need to explicitly specify the typecast operator.

Examples:

Code Description
byte a = 110;
byte b = 120;
byte c = (byte) (a * b);
The byte * byte expression will be an int
byte a = 110;
byte b = 120;
byte c = (byte) (a + b);
The byte + byte expression will be an int
byte a = 1;
byte b = (byte) (a + 1);
The byte + int expression will be an int
The literal one is an int.

5. An important nuance

The typecast operator has a fairly high priority.

That means that if an expression contains, for example, addition and a typecast operator, the typecast will be performed before the addition.

Example:

Code Description
byte a = 1;
byte b = 2;
byte c = (byte) a * b;
The typecast operator will only be applied to the a variable, which is already a byte. This code will not compile.
byte a = 1;
byte b = 2;
byte c = (byte) (a * b);
This is the correct way.

If you want to convert the entire expression to a specific type, and not just one component of the expression, then wrap the entire expression in parentheses and put the typecast operator in front.