تحويل الأنواع والتحويل القسري (casting)
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
So far each box held a value of exactly its own type. Real programs mix them all the time: a whole-number quantity times a decimal price, a total divided among a number of people, a big count squeezed into a smaller box. When types meet, Java converts values from one type to another. Some conversions are safe and happen automatically. Others can destroy information, and Java makes you ask for them explicitly with a cast.
Keep the box picture. Moving a value from a small box to a bigger one always fits, like pouring a cup into a bucket. Moving from a big box to a smaller one may not fit, and something spills. This lesson teaches you to predict the spill, including the surprising case where a whole number runs past its limit and wraps around.
Why this matters: conversion bugs are among the most common silent bugs in real code. They do not crash; they produce a plausible but wrong number, such as an average of 2 instead of 2.5, or a negative count of views. Spotting them is a core skill.
This lesson builds on Lessons 1 to 4, especially the sizes and ranges of the primitive types in Lesson 2.
Learning Objectives
By the end of this lesson you will be able to:
- Explain widening conversions (small box to bigger box) and why they happen automatically.
- Explain narrowing conversions (big box to smaller box), and write a cast.
- Predict that casting a decimal to an
intcuts off the fraction (it does not round). - Predict the result of integer division and fix it when decimals are wanted.
- Explain integer overflow and predict the wrapped result, and avoid it with
long. - Read the compiler message
possible lossy conversion.
Written Explanation
Widening: small box to bigger box
A widening conversion goes from a type to one that can hold at least the same range of values. The compiler performs it automatically, with no extra syntax:
byte -> short -> int -> long -> float -> double char -> intint whole = 7;
double wide = whole; // 7 becomes 7.0, automaticallyWidening between integer types is exact. Widening an int to a double is also exact. The Language Specification warns about one subtle case: converting an int or long to float, or a long to double, can lose some of the least significant digits, because a float has only about seven significant decimal digits of precision. So "widening" means "bigger range", not always "perfect copy". Example 3 shows it.
Narrowing: big box to smaller box
A narrowing conversion goes the other way, for example double to int, long to int, or int to byte. Java does not do these automatically, because information may be lost. If you try, the compiler stops you:
error: incompatible types: possible lossy conversion from double to intIf you really mean it, you write a cast: the target type in parentheses before the value.
double pi = 3.99;
int cut = (int) pi; // 3The cast says "I know this may lose information, and I accept that". Two rules of what happens:
- Decimal to whole number: the fraction is cut off, not rounded.
(int) 3.99is3, and(int) -3.99is-3: the result moves toward zero. If you want rounding, the standard methodMath.round(x)rounds to the nearest whole number (it returns along). - Whole number to a smaller whole type: only the low bits are kept. The value can change completely, including its sign. For example
(byte) 130is-126.
Integer division
When both operands of / are integers, the result is an integer: the fraction is dropped. So 7 / 2 is 3, not 3.5. The remainder operator % gives what is left over: 7 % 2 is 1.
If either operand is a double, the other is widened and the division is a decimal one: 7 / 2.0 is 3.5.
Where you put the cast matters:
| Expression | Result | Why |
|---|---|---|
7 / 2 | 3 | integer division |
7 / 2.0 | 3.5 | one side is double |
(double) 7 / 2 | 3.5 | the cast applies to 7 first, then decimal division |
(double) (7 / 2) | 3.0 | the integer division happens first, the cast comes too late |
A frequent beginner bug is double share = total / people; with two ints. The division is done in integers, then the result is widened to double. The decimals were already lost.
Integer overflow
Every whole-number type has a maximum. What happens when you go past it? Java does not raise an error. The result wraps around: it keeps only what fits, as if an odometer rolled past its largest number.
int max = Integer.MAX_VALUE; // 2147483647
System.out.println(max + 1); // -2147483648The Language Specification says integer operators never signal overflow; results wrap, keeping the low-order bits. Adding 1 to the largest int gives the smallest. This is why overflow is a silent bug: the program continues with a wrong, often negative, number.
Overflow also hides in multiplication. 1_000_000 * 1_000_000 is a trillion, which is far outside int. Because both operands are int, Java does the whole multiplication as int and wraps. Assigning the result to a long variable afterwards does not help, because the damage is done before the assignment. The fix is to make one operand long first:
long ok = (long) a * a; // a is widened to long, so the multiplication is done as longWhen arithmetic mixes types, Java first widens both operands to a common type (int with long gives long; anything with double gives double), then calculates in that type.
Seeing overflow instead of hiding it
If wrapping would be a bug, the standard library provides Math.addExact(a, b) and similar methods that throw an ArithmeticException (message integer overflow) instead of wrapping. Using them is a later-module topic; it is enough to know they exist.
Examples
Example 1: widening, casting, and rounding (simple)
public class Casts {
public static void main(String[] args) {
int whole = 7;
double wide = whole;
System.out.println(wide);
double pi = 3.99;
int cut = (int) pi;
System.out.println(cut);
System.out.println((int) -3.99);
System.out.println(Math.round(3.99));
System.out.println(Math.round(2.5));
}
}Output (JDK 21):
7.0
3
-3
4
3Widening gave 7.0 automatically. The cast cut 3.99 to 3, and -3.99 to -3 (toward zero, not down). Math.round really rounds: 3.99 becomes 4, and 2.5 becomes 3.
Example 2: division that loses its decimals (practical)
Four friends share a bill of 10:
public class Share {
public static void main(String[] args) {
int total = 10, people = 4;
double share = total / people;
System.out.println(share);
System.out.println((double) total / people);
System.out.println(7 / 2);
System.out.println(7 / 2.0);
System.out.println((double) (7 / 2));
}
}Output:
2.0
2.5
3
3.5
3.0The first line is the trap: the variable is a double, yet the answer is 2.0, because total / people was an integer division that gave 2 before the widening. Casting one operand first (second line) gives the correct 2.5. The last line shows a cast that is too late.
Example 3: overflow, narrowing, and a lossy widening (realistic)
public class Overflow {
public static void main(String[] args) {
int max = Integer.MAX_VALUE;
System.out.println(max + 1);
System.out.println(Integer.MIN_VALUE - 1);
int a = 1_000_000;
System.out.println(a * a);
long ok = (long) a * a;
System.out.println(ok);
long wrong = a * a;
System.out.println(wrong);
long big = 3_000_000_000L;
int shrunk = (int) big;
System.out.println(shrunk);
System.out.println((byte) 130);
System.out.println((int) 1e20);
int i = 1_234_567_890;
float f = i;
System.out.println((int) f);
}
}Output:
-2147483648
2147483647
-727379968
1000000000000
-727379968
-1294967296
-126
2147483647
1234567936Line by line: the maximum plus one wraps to the minimum, and the minimum minus one wraps to the maximum. a * a wraps to -727379968, the same value the Language Specification itself uses to illustrate overflow. The long version with a cast before the multiplication gives the true trillion; the "wrong" version assigns to a long but still wraps, because the multiplication was done in int. Squeezing three billion into an int gives -1294967296. (byte) 130 is -126. (int) 1e20 does not wrap: converting a too-large decimal to an int clamps to the maximum, 2147483647. The last line shows widening int to float losing digits: 1234567890 came back as 1234567936.
Backend Insight
- Averages, percentages, and ratios. Computing an average as
sum / countwith two integers silently truncates. A dashboard that shows2instead of2.5, or a conversion rate of0, is usually this bug. Make one operand decimal before dividing. - Counters and ids that overflow. A counter of views or a running total kept in an
intcan wrap to a negative number after about 2.1 billion. Uselongfor anything that can grow without a clear limit. - Mixed units. Multiplying
24 * 60 * 60 * 1000 * 365(milliseconds in a year) inintarithmetic overflows. Written as24L * 60 * 60 * 1000 * 365, the firstLmakes the whole chainlongand gives the correct31536000000. - Boundaries between systems. Data from a database or a request often arrives as a larger type than your variable. A cast makes the narrowing visible in code, so reviewers can ask "can this value exceed the range?"
Defensive habit: when narrowing, check that the value fits first, or use an exact-arithmetic helper such as Math.addExact when wrapping would be a bug.
Common Mistakes
Mistake 1: Expecting a decimal from integer division
int average = 7 / 2; is 3. If you want 3.5, at least one operand must be a double, and the variable must be a double too.
Mistake 2: Casting too late
(double) (a / b) converts the already-truncated integer. Cast an operand: (double) a / b.
Mistake 3: Expecting a cast to round
(int) 9.99 is 9. The cast discards the fraction. Use Math.round if you want the nearest whole number.
Mistake 4: Trying to assign a wider value to a narrower variable without a cast
double d = 9.5;
int i = d;The compiler (JDK 21) says incompatible types: possible lossy conversion from double to int. The same message appears for long to int, double to float, and int * 1.5 stored back into an int. Either change the variable to the wider type, or write a cast knowing the cost.
Mistake 5: Assuming small types stay small in arithmetic
byte b = 10;
b = b + 1; // error: possible lossy conversion from int to byteArithmetic on byte, short, and char is done in int, so the result is an int and must be cast back. This is one reason beginners should prefer int. Similarly char c = 'A'; c = c + 1; does not compile without (char).
Mistake 6: Believing overflow will warn you
Overflow does not warn or crash. A program can print -2147483648 and carry on. Whenever numbers can get large, ask whether int is big enough.
Exercises
Try each exercise yourself before opening the solutions. Compile and run them if you have a JDK.
Level A: Foundation
A1. Which conversion needs a cast: int to long, or long to int? Why?
أظهر الحل A1
A1. long to int needs a cast, because the long may hold values the int cannot. int to long is a widening conversion and is automatic.
A2. What do 9 / 4, 9 / 4.0, and 9 % 4 evaluate to?
أظهر الحل A2
A2. 2, 2.25, and 1.
A3. What is (int) 5.9, and what is (int) -5.9?
أظهر الحل A3
A3. 5 and -5: the fraction is cut off toward zero.
Level B: Application
B1. A student has int points = 17; and int tests = 4;. Write the line that computes average as a double, correct to the decimal part, and give its value.
أظهر الحل B1
B1. double average = (double) points / tests; gives 4.25. Writing points / tests first would give 4.
B2. Predict the output:
byte small = (byte) 127;
small = (byte) (small + 1);
System.out.println(small);أظهر الحل B2
B2. 127 + 1 is 128, which does not fit a byte. Keeping the low 8 bits gives -128, so it prints -128. The wrap-around is the same idea as with int, applied to a smaller box.
B3. Explain why long seconds = 60 * 60 * 24 * 365 * 100; is risky, and rewrite it safely. (A hundred years of seconds is about 3.15 billion.)
أظهر الحل B3
B3. All the literals are int, so the multiplication runs in int arithmetic and the result (about 3.15 billion) exceeds the int maximum before it is stored in the long. Fix: make one operand long first, long seconds = 60L * 60 * 24 * 365 * 100;. It gives 3153600000.
Level C: Challenge
C1. A program reads int widthPx = 1920; and int heightPx = 1080; and wants the aspect ratio as a decimal (about 1.78). The programmer writes double ratio = widthPx / heightPx; and gets 1.0. Explain exactly where the information is lost and give two correct fixes.
أظهر الحل C1
C1. Both operands are int, so 1920 / 1080 is an integer division: it gives 1. That 1 is then widened to 1.0. The decimals were lost before the assignment. Fixes: (double) widthPx / heightPx, or widthPx / (double) heightPx, or change one variable to double. Either gives about 1.78.
C2. int a = 2_000_000_000; int b = 2_000_000_000; long sum = a + b; prints a negative number. Explain why, predict the value, and write two fixes.
أظهر الحل C2
C2. Both operands are int, so a + b is computed as an int. The true sum is 4,000,000,000, which is more than Integer.MAX_VALUE, so it wraps to 4,000,000,000 - 4,294,967,296 = -294,967,296. Assigning to a long afterwards cannot recover the lost value. Fixes: long sum = (long) a + b;, or declare a and b as long.
Mini Challenge
Write a program Checks that prints, with a short label each: (1) the average of the three numbers 7, 8, and 10 as a double; (2) the number of seconds in 30 days, using a long; (3) Integer.MAX_VALUE + 1 with a one-line comment saying why the result is negative; (4) the result of narrowing 2.99 to an int.
Check your work: the average is about 8.33 and not 8; the seconds figure is 2592000; the overflow comment mentions that the value wraps around; and the cast result is 2, not 3.
Summary & Next Lesson
Key takeaways
- Widening (small to big, for example
inttodouble) is automatic; narrowing (big to small) needs a cast and may lose information. - A cast from decimal to whole cuts off the fraction; it does not round.
Math.roundrounds. /on two integers is integer division:7 / 2is3. Make one operanddoubleto get3.5, and do it before dividing.- Integer overflow wraps around silently:
Integer.MAX_VALUE + 1is-2147483648. - Use
longwhere values can get large, and make an operandlongbefore the arithmetic.
Next lesson
You can now predict what happens when types mix. The next lesson, "Local type inference with var", shows how to let the compiler work out a local variable's type from its value, and when that helps or hurts readability.
Self-check
- Is
double d = 5;allowed without a cast? Why? - What is
(int) 2.99? - What is
5 / 2, and how do you get2.5? - What does
Integer.MAX_VALUE + 1evaluate to? - Why does storing
a * ain alongnot fix an overflow ina * awhenais anint?
أظهر الإجابات
- Yes:
inttodoubleis a widening conversion, which is automatic. 2.2. Use5 / 2.0or(double) 5 / 2.-2147483648: the value wraps around.- The multiplication is performed in
intarithmetic before the result is stored, so it has already wrapped. Make one operandlongfirst.
References
- Oracle, The Java Language Specification, Java SE 21, Chapter 5: Conversions and Contexts. It lists the widening conversions and notes that
inttofloatandlongtofloat/doublecan lose precision, says narrowing from a floating-point type to an integer type truncates toward zero and clamps values that are too large (for example(int) 1e20fis theintmaximum), and says narrowing between integer types keeps only the low-order bits (so(byte) 255is-1). - Oracle, The Java Language Specification, Java SE 21, section 4.2.2: Integer Operations. It says integer operators never signal overflow and that results wrap, and it uses
1000000 * 1000000as an example producing-727379968. - Oracle, The Java Tutorials: Primitive Data Types. It gives the sizes and ranges of the integer types.
- Every output above, including
ArithmeticException: integer overflowfromMath.addExact, was produced by running the code with JDK 21, and the exercise answers were checked the same way.Math.roundbehavior was observed in the run, not read from its documentation.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.