Converting Between Types
A value of one type can sometimes be converted to a different type. Programming languages provide several different pathways for this conversion. A conversion that is implicit and performed automatically by the compiler or interpreter is a coercion. A conversion that is explicit and triggered by the programmer is a cast.
Java orders its numeric primitives by their information capacity in the following way:
-
double
-
float
-
long
-
int
-
short
-
byte
If a value whose type is lower on this list is pressed into a type that is higher, the compiler will implicitly coerce the value. This is a widening conversion. For example, a byte
can freely be assigned to an int
and a float
to a double
. Converting in the other direction, a narrowing conversion, is not automatic, as information may be lost. For example, there are many int
values that do not fit in a byte
. The programmer must acknowledge the risk of this information loss by inserting an explicit cast:
int i = 659;
byte b = (byte) i;
In truth, even an implicit coercion can lead to information loss. Consider this Java program:
int i = 16777217; // 2 ^ 24 + 1
float f = i; // coerce int to float
System.out.printf("f: %.0f", f); // prints 16777216
The number 16,777,217 is a legal int
, but it cannot be represented as a float
. The Java designers consider the loss of lower-order bits in a widening conversion neglible compared to the loss of higher-order bits in a narrowing conversion, so only narrowing conversions require a cast.
Other languages take a firmer stance and perform very few automatic coercions. In Kotlin, for example, even assigning a 4-byte int
to a much wider 8-byte long
requires an explicit cast:
val small: Int = 100
// val big: Long = small <- illegal coercion
val big: Long = small.toLong()
C++ provides an extensive casting and coercion system. This code seems like it should be an obvious compilation error:
Square s = 8;
But this code is perfectly legal—provided the class definition of Square
includes a converting constructor whose formal parameter matches the right-hand side of the assignment statement, as this definition does:
class Square {
public:
Square(int size) : size(size) {}
private:
int size;
};
Such implicit coercions are not always considered safe. They can be disabled by qualifying a constructor as explicit
:
class Square {
public:
explicit Square(int size) : size(size) {}
private:
int size;
};
Instantiating a Square
must now be done with one of the several explicit construction forms:
// Square s = 8; <- illegal
Square s = Square(8);
Square t(8);
Square u {8};
This code also seems like it should fail to compile:
Square s {8};
int sum = 10 + s;
Surely a Square
cannot be added to an int
? Yes, it can, provided the Square
class overloads the int
cast operator:
class Square {
public:
Square(int size) : size(size) {}
operator int() const {
return size;
}
private:
int size;
};
Ruby, Python, and JavaScript support a mixture of implicit and explicit conversions. If we want a value to change its type in order to apply different operations, we must cast it. Casting primitives in Python is done with constructor functions:
pint = int(3.14149)
half = float("0.5")
decade = str(1990) + "s"
Ruby provides casting methods:
pint = 3.14149.to_i
half = "0.5".to_f
decade = 1990.to_s + "s"
JavaScript provides a mix of constructor functions and methods that perform casting:
pint = Math.trunc(3.14159)
half = Number("0.5")
// 1990.toString() <- illegal: literals don't have methods
decade = Number(1990).toString() + "s"
Different languages handle failed conversions in different ways. In Java, if we attempt to cast an object to an incompatible class, a ClassCastException
is thrown. In C++, we'll get null when converting a pointer or a bad_cast
exception when converting a reference. Casting the string "ix"
to a float yields an exception in Python, 0.0
in Ruby, and NaN
in JavaScript.