Java Module 2 – Variables, types and operators
char, boolean, casting and conversions
Widening and narrowing casts in Java, char arithmetic such as 'a' + 1, why (byte) 200 is -56 and (int) 3.99 is 3, and the hidden cast inside +=.
What you will learn
- Compute with char values and convert between char and int
- Explain why a boolean never converts to or from a number, and write the conversion you mean
- Predict which widening conversions keep a value and which can round it
- Predict truncation and sign changes when a narrowing cast drops bits
- Explain the cast hidden in compound assignment and the numeric promotion behind it
Before you start
What you’ll learn · Before you start
What you will learn
- Compute with char values and convert between char and int
- Explain why a boolean never converts to or from a number, and write the conversion you mean
- Predict which widening conversions keep a value and which can round it
- Predict truncation and sign changes when a narrowing cast drops bits
- Explain the cast hidden in compound assignment and the numeric promotion behind it
Before you start
On this page
Java converts between numeric types in two directions. A
widening conversion: A conversion to a type that holds every value of the old one, such as int to long or char to int, which Java applies on its own. Between integer types it never changes the value; int or long to float and long to double can round., such as int to long or char to int, goes to a type
whose range covers every value of the old one, so Java applies it on its own. A
narrowing conversion: A conversion to a type that may not hold the value, such as int to byte or double to int. It needs a cast: integers keep their lowest bits, and floating-point values are cut toward zero and clamped to the range. goes the other way and needs a
cast: An explicit conversion written as (type) value, such as (byte) 200 or (int) 3.99. A cast between primitive types never throws, even when the value changes: (byte) 200 is -56., such as (byte) 200 or (int) 3.99, because the value may change: integers keep only
their lowest bits, so (byte) 200 is -56, and floating-point values lose their fraction, so (int) 3.99 is 3.
Two types need their own rules. A char is a number, the 16-bit UTF-16 code of a character, so 'a' + 1 is the
int 98. A boolean is not a number at all, and no cast turns it into one. You will run every kind of conversion on
JDK 25, step through the bits a cast keeps and the cast hidden inside +=, and finish by shifting letters with char
arithmetic.
A char is a number
A char holds one UTF-16 code unit: One 16-bit number of the UTF-16 encoding of text, the unit a Java char holds and String.length() counts. Most characters are one code unit; characters beyond the first 65,536, such as many emoji, take two., an unsigned number from 0 to 65,535. Most
characters you type, Latin and Devanagari letters included, are one code unit each, so for them a char is simply the
character’s code:
// A char is a number: the UTF-16 code of one character, from 0 to 65535.
void main() {
char letter = 'a';
IO.println("'a' + 1 = " + ('a' + 1)); // char + int is an int
IO.println("(char) ('a' + 1) = " + (char) ('a' + 1));
letter++; // ++ keeps the char type
IO.println("after letter++ = " + letter);
IO.println("codes of A, Z, a, 0: " + (int) 'A' + " " + (int) 'Z' + " " + (int) 'a' + " " + (int) '0');
IO.println("'7' - '0' = " + ('7' - '0')); // a digit character to its value
IO.println("'a' + 'b' = " + ('a' + 'b')); // two chars add up as numbers
IO.println("\"\" + 'a' + 'b' = " + ("" + 'a' + 'b')); // after a String, + joins text
char ka = 'क';
IO.println(ka + " is " + Character.getName(ka) + ", code " + (int) ka + ", a letter: " + Character.isLetter(ka));
String smile = "😀";
IO.println(smile + " is " + smile.length() + " chars but " + smile.codePointCount(0, smile.length()) + " code point");
} from session import run, version
version()
run("""
java Chars.java
java Booleans.java
!java BooleanErrors.java
""") Output
# openjdk version "25.0.4.1" LTS
$ java Chars.java
'a' + 1 = 98
(char) ('a' + 1) = b
after letter++ = b
codes of A, Z, a, 0: 65 90 97 48
'7' - '0' = 7
'a' + 'b' = 195
"" + 'a' + 'b' = ab
क is DEVANAGARI LETTER KA, code 2325, a letter: true
😀 is 2 chars but 1 code point
$ java Booleans.java
empty: false, ready: true
as a number: 1
parseBoolean: true false false
$ java BooleanErrors.java
BooleanErrors.java:3: error: incompatible types: int cannot be converted to boolean
if (items) { // an int is not a condition
^
BooleanErrors.java:6: error: incompatible types: int cannot be converted to boolean
boolean ready = 1; // 1 is not true
^
BooleanErrors.java:7: error: incompatible types: boolean cannot be converted to int
int flag = (int) ready; // and no cast turns a boolean into a number
^
3 errors
error: compilation failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 chars_session.py
'a' + 1is theint98: arithmetic turns acharinto anintfirst. Cast the result back,(char) ('a' + 1), to get the characterb.letter++keeps the type: increment and decrement operators, like compound assignment below, convert the result back to the variable’s type.'7' - '0'is 7, because the digit characters have consecutive codes, 48 to 57.Character.getNumericValuedoes the same for digits of other scripts too.'a' + 'b'adds two codes and prints 195. Once aStringstands on the left,+joins text instead, so"" + 'a' + 'b'is"ab".'क'is a Devanagari letter with the code 2325, andCharacter.isLetterknows it. Since JDK 18 Java reads source files as UTF-8 by default, so such letters can stand in the code as they are.- The emoji 😀 is outside the first 65,536 code points, so it takes two
charvalues, a surrogate pair: Two UTF-16 code units, a high one from D800 to DBFF and a low one from DC00 to DFFF, that together encode one character beyond U+FFFF, such as an emoji. length() counts both; codePointCount counts one.:length()counts code units,codePointCountcounts characters. A later lesson on Unicode covers this properly.
The codes the run printed follow a pattern worth knowing by heart:
| Character | Its code | What follows from it |
|---|---|---|
A |
65 | A to Z are 65 to 90 |
a |
97 | each lower-case letter is 32 above its capital |
0 |
48 | '7' - '0' is the digit’s value, 7 |
क |
2325 | one code unit, like every Devanagari letter |
😀 |
two code units | length() counts 2 for it |
Checkpoint
Check: A char is a number
A boolean is not a number
boolean has exactly two values, true and false, and Java has no conversion between it and any number. A
condition must be a boolean, so the C habit of if (count) does not compile, and neither does boolean ready = 1
or a cast such as (int) ready. The last command of the session above shows all three errors, and each has a
direct way to say what was meant:
| Written as in C | What the compiler says | Write instead |
|---|---|---|
if (items) |
int cannot be converted to boolean | if (items != 0) |
boolean ready = 1; |
int cannot be converted to boolean | boolean ready = true; |
int flag = (int) ready; |
boolean cannot be converted to int | int flag = ready ? 1 : 0; |
Booleans come from comparisons and logic: items == 0 and !empty && items < 10 are boolean expressions. When you
need 1 or 0, say so with the conditional operator, ready ? 1 : 0. Boolean.parseBoolean returns true only for the
text “true” in any mix of upper and lower case; “yes”, “1” and null all give false.
Checkpoint
Check: A boolean is not a number
Widening happens on its own
Java applies a widening conversion wherever one is needed: in an assignment, when passing an argument, and inside an expression. The diagram shows every widening path: follow the arrows from a type to the types it widens to.
Widening conversions, and the three that can round the value
Text description of the diagram
Six boxes stand in a row, byte, short, int, long, float and double, with arrows from each to the next; a seventh box, char, has an arrow to int. Java applies these conversions on its own, and any path along the arrows is allowed: byte to long, char to double and so on.
- Solid arrows keep the value exactly: byte to short, short to int, char to int, int to long, float to double, and the arc from int to double.
- Dashed arrows may round the value to the nearest floating-point number: long to float, the arc from int to float, and the arc from long to double. A float has 24 significant bits and a double 53, so an int or long with more significant bits than that cannot always be held exactly.
Widening never throws, and between integer types it never changes the value. Three widenings can still round, the
dashed arrows of the diagram, because a float has 24 significant bits and a double 53:
// Widening conversions happen on their own; three of them can round the value.
void main() {
byte small = 100;
int i = small; // byte to int
long l = i; // int to long
double d = l; // long to double
char c = 'A';
int code = c; // char to int
IO.println(i + " " + l + " " + d + " " + code);
int big = 16_777_217; // 2 to the power 24, plus 1
float f = big; // int to float compiles, but a float has 24 bits of precision
IO.println(big + " as a float = " + f + ", back to int = " + (int) f);
long huge = 9_007_199_254_740_993L; // 2 to the power 53, plus 1
double asDouble = huge; // long to double: 53 bits of precision
IO.println(huge + " as a double, back to long = " + (long) asDouble);
} from session import run, version
version()
run("""
java Widening.java
java Narrowing.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Widening.java 100 100 100.0 65 16777217 as a float = 1.6777216E7, back to int = 16777216 9007199254740993 as a double, back to long = 9007199254740992 $ java Narrowing.java (byte) 200 = -56 (byte) 256 = 0 (byte) -129 = 127 (short) 70000 = 4464 (int) 3000000000L = -1294967296 (char) 66 = B (int) (char) -1 = 65535 (int) 3.99 = 3 (int) -3.99 = -3 (int) 1e10 = 2147483647 (int) Double.NaN = 0 (byte) 300.7 = 44 200 in bits: 11001000, as a byte: -56 300 in bits: 100101100, as a byte: 44
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 casts_session.py
16,777,217 is 2²⁴ + 1, which needs 25 significant bits, so as a float it becomes the nearest float, 16,777,216, and
the cast back to int shows the change. 9,007,199,254,740,993, which is 2⁵³ + 1, loses its last bit the same way as
a double. Most code never meets numbers that large, but IDs, time stamps in nanoseconds and money in the smallest
unit can, so keep them in long and never send them through float or double.
Key idea
Widening between integer types never changes a value; int or long to float, and long to double, compile
without a cast and can still round a large value to the nearest one the type holds.
Checkpoint
Check: Widening conversions
Narrowing needs a cast
A conversion that can lose information must be asked for with a cast, (type) value. What it keeps depends on the
source type. The second program of the session above, Narrowing.java, shows each case:
// Narrowing casts: integers keep their low bits, floating-point values are cut toward zero first.
void main() {
IO.println("(byte) 200 = " + (byte) 200);
IO.println("(byte) 256 = " + (byte) 256);
IO.println("(byte) -129 = " + (byte) -129);
IO.println("(short) 70000 = " + (short) 70_000);
IO.println("(int) 3000000000L = " + (int) 3_000_000_000L);
IO.println("(char) 66 = " + (char) 66);
IO.println("(int) (char) -1 = " + (int) (char) -1);
IO.println("(int) 3.99 = " + (int) 3.99);
IO.println("(int) -3.99 = " + (int) -3.99);
IO.println("(int) 1e10 = " + (int) 1e10);
IO.println("(int) Double.NaN = " + (int) Double.NaN);
IO.println("(byte) 300.7 = " + (byte) 300.7);
IO.println("200 in bits: " + Integer.toBinaryString(200) + ", as a byte: " + (byte) 200);
IO.println("300 in bits: " + Integer.toBinaryString(300) + ", as a byte: " + (byte) 300);
} - Integer to a smaller integer: the low bits stay. 200 is 11001000 in binary. A
bytekeeps those 8 bits and reads them as a signed number, -56. 256 is 1 followed by eight zeros, so(byte) 256is 0, and(short) 70000keeps the low 16 bits, 4,464. The sign can flip either way:(byte) -129is 127. - Integer to
char: also the low bits, read as unsigned.(char) -1is thechar65,535. - Floating point to an integer: cut toward zero, then clamp.
(int) 3.99is 3 and(int) -3.99is -3. A value beyond the range becomes the nearest limit, so(int) 1e10is 2,147,483,647, and NaN becomes 0. - Floating point to
byte,shortorchar: two steps. The value is cut to anintfirst, then narrowed as an integer: 300.7 becomes 300, and 300 as abyteis 44.
Step through the bits behind two of those lines, using the binary forms the last two lines of the run print:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 200 in an int. 200 is an int, 32 bits wide. In binary it is 11001000 with 24 zeros in front, as the line 200 in bits of the run shows.
- Keep 8 bits. (byte) 200 keeps the low 8 bits, 1100 1000, and drops the other 24. Nothing is checked and nothing is thrown.
- Read them signed. A byte reads its highest bit as minus 128, so 1100 1000 is −128 + 64 + 8 = -56. The positive 200 has become a negative byte.
- A double goes to int first. A cast from double to byte takes two steps. First the double is cut toward zero to an int, so 300.7 becomes 300.
- Then 8 bits. 300 is 100101100 in binary, as the last line of the run prints. The low 8 bits are 0010 1100, which is 32 + 8 + 4 = 44.
- Check before you cut. Neither cast throws, and neither warns. When a value that does not fit is a bug, test the range first or use Math.toIntExact for a long.
None of these throws. When a value that does not fit is a bug, check first: Math.toIntExact throws for a long
outside the int range, and Math.round rounds a double to the nearest long instead of cutting it.
Checkpoint
Check: Narrowing casts
The cast hidden in compound assignment
b += 300 is not the same as b = b + 300. A compound assignment: An assignment operator such as +=, -= or *=: b += x means b = (T) (b + x), where T is the type of b, so the result is cast back to the variable's type, and narrowed without a warning. such as
+=, -= or *= casts its result back to the type of the variable, so it compiles for a byte even when the
result does not fit:
Java (Compound.java) · Compound.java
// A compound assignment casts its result back to the type of the variable.
void main() {
byte b = 10;
b += 300; // b = (byte) (b + 300): 310 does not fit, 54 is kept
IO.println("byte 10 += 300 -> " + b);
short s = 1000;
s *= 1000; // 1,000,000 as a short
IO.println("short 1000 *= 1000 -> " + s);
int total = 5;
total += 2.7; // total = (int) (5 + 2.7)
IO.println("int 5 += 2.7 -> " + total);
char c = 'a';
c += 2;
IO.println("char 'a' += 2 -> " + c);
final byte x = 10;
byte y = x + 20; // a constant expression that fits: allowed
IO.println("final byte x = 10; byte y = x + 20 -> " + y);
} Java (CompoundErrors.java) · CompoundErrors.java
void main() {
byte b = 10;
b = b + 300; // b + 300 is an int
byte x = 10, y = 20;
byte z = x + y; // so is x + y, even though 30 would fit
char c = 'a';
c = c + 1; // and c + 1
} from session import run, version
version()
run("""
java Compound.java
!java CompoundErrors.java
javac -Xlint:lossy-conversions Compound.java
""") Output
# openjdk version "25.0.4.1" LTS
$ java Compound.java
byte 10 += 300 -> 54
short 1000 *= 1000 -> 16960
int 5 += 2.7 -> 7
char 'a' += 2 -> c
final byte x = 10; byte y = x + 20 -> 30
$ java CompoundErrors.java
CompoundErrors.java:3: error: incompatible types: possible lossy conversion from int to byte
b = b + 300; // b + 300 is an int
^
CompoundErrors.java:5: error: incompatible types: possible lossy conversion from int to byte
byte z = x + y; // so is x + y, even though 30 would fit
^
CompoundErrors.java:7: error: incompatible types: possible lossy conversion from int to char
c = c + 1; // and c + 1
^
3 errors
error: compilation failed
[exit status 1]
$ javac -Xlint:lossy-conversions Compound.java
Compound.java:4: warning: [lossy-conversions] implicit cast from int to byte in compound assignment is possibly lossy
b += 300; // b = (byte) (b + 300): 310 does not fit, 54 is kept
^
Compound.java:10: warning: [lossy-conversions] implicit cast from double to int in compound assignment is possibly lossy
total += 2.7; // total = (int) (5 + 2.7)
^
2 warnings
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compound_session.py
10 + 300 is 310, and the hidden cast to byte keeps its low 8 bits, 54. short 1,000 times 1,000 becomes 16,960
the same way, and total += 2.7 on an int drops the fraction. By default the compiler accepts all of these without
a warning, so be careful with += on narrow types. Step through both forms side by side:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The same sum, twice. b is a byte holding 10. b += 300 on the left and b = b + 300 on the right both start by adding 300 to b.
- Numeric promotion. Arithmetic promotes a byte to int before it adds, so b + 300 is the int 310 in both columns. 310 does not fit in a byte, which stops at 127.
- The plain assignment is refused. b = b + 300 would store an int in a byte, a narrowing conversion that needs a cast. The compiler refuses it: possible lossy conversion from int to byte.
- The hidden cast. A compound assignment is defined as b = (byte) (b + 300). The cast is part of the operator, so this line compiles, by default without a warning.
- 54, without a word. The cast keeps the low 8 bits of 310, which is 310 − 256 = 54. The run printed byte 10 += 300 -> 54.
- The rule to keep. Every +=, -= and *= on a byte, short or char hides a cast. Use a wider variable when the result can grow, and write the cast yourself when you mean it.
The errors in CompoundErrors.java come from numeric promotion: The rule that converts the operands of an arithmetic operator to a common type before it computes: byte, short and char become at least int, so the sum of two bytes is an int, and an int and a long give a long.: the operands of
+, -, * and the other arithmetic operators are promoted to at least int, so b + 300, x + y on two bytes
and c + 1 on a char are all int, and an int does not go into a byte or a char without a cast. A constant
expression is the exception: final byte x = 10; makes x + 20 a constant, 30, which the compiler knows fits, so
byte y = x + 20; compiles.
javac can be asked to warn. The last command of the session compiles Compound.java with
-Xlint:lossy-conversions, which flags b += 300 and total += 2.7, whose right-hand side would need a cast to be
assigned to the variable. It says nothing about s *= 1000 or c += 2, whose constants fit, although the first one
wraps. The lint is worth turning on in a build, but only a variable wide enough for the result prevents the wrap.
Counting with += in a byte or a short
short count = 0; with count += 1; in a loop compiles and runs until count passes 32,767, then it wraps to
-32,768 without a word, because every += hides a cast. Count in an int or a long.
Checkpoint
Check: Promotion and compound assignment
Practise: a Caesar shift
Write shift(text, k), which moves every letter from a to z and A to Z k places along the alphabet, wrapping round
from z to a, and leaves every other character as it is. k may be negative or very large:
// Exercise: shift(text, k) moves every letter a to z and A to Z k places along the alphabet, wrapping round from z
// to a, and leaves every other character as it is; k may be negative or very large. Write it, then run this file:
// every check must say ok.
String shift(String text, int k) {
return text; // replace this line
}
int failures = 0;
void check(String text, int k, String expected) {
String got = shift(text, k);
if (!got.equals(expected)) failures++;
IO.println((got.equals(expected) ? "ok " : "FAILED ") + "shift(\"" + text + "\", " + k + ") = \"" + got + "\"");
}
void main() {
check("abc", 1, "bcd");
check("xyz", 3, "abc");
check("Hello, World!", 13, "Uryyb, Jbeyq!");
check("abc", -1, "zab");
check("Java 25", 1_000_000, "Xojo 25");
check("Java", -1_000_000, "Vmhm");
check("Zebra", Integer.MAX_VALUE, "Wbyox");
check("café, नमस्ते", 2, "eché, नमस्ते");
IO.println(failures + " of 8 checks failed");
if (failures > 0) System.exit(1);
} As given, the method returns the text unchanged:
from session import run, version
version()
run("""
!java Caesar.java
""") Output
# openjdk version "25.0.4.1" LTS
$ java Caesar.java
FAILED shift("abc", 1) = "abc"
FAILED shift("xyz", 3) = "xyz"
FAILED shift("Hello, World!", 13) = "Hello, World!"
FAILED shift("abc", -1) = "abc"
FAILED shift("Java 25", 1000000) = "Java 25"
FAILED shift("Java", -1000000) = "Java"
FAILED shift("Zebra", 2147483647) = "Zebra"
FAILED shift("café, नमस्ते", 2) = "café, नमस्ते"
8 of 8 checks failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
First hint: reduce k first with Math.floorMod(k, 26) from the integer arithmetic lesson, then for a lowercase
letter compute (char) ('a' + (c - 'a' + step) % 26). Adding k before reducing it overflows for
Integer.MAX_VALUE, which the seventh check catches.
When the eight shifts all say ok, take the questions below on codes, booleans, widening, the bits a cast keeps and
the cast inside +=. A question you get wrong returns at the end of the round, with its reasons:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · Why does byte b = 10; b = b + 1; fail to compile while b += 1; works?
In b + 1 both operands are promoted to int, so the sum is an int, and assigning an int to a byte is a
narrowing conversion that needs a cast. A compound assignment is defined as b = (byte) (b + 1): it includes that
cast, so it compiles, and it silently keeps only the low 8 bits when the result does not fit, so b += 300 turns 10
into 54. Write the cast yourself when narrowing is what you want, and use a wider variable when it is not.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A checksum adds the bytes of a file with int sum = 0; for (byte b : data) sum += b;. For some files it disagrees with the checksum another program computes for the same file. Find the bug, fix it, and say which files expose it.
-
Fresher to mid
Prices arrive from a JSON feed as doubles and are turned into cents with (int) (price * 100). Some totals come out one cent short. Explain why, say how many prices are affected, and design a conversion that is always right.
Key takeaways
- A
charis an unsigned 16-bit number:'a' + 1is theint98, and(char) ('a' + 1)is'b'. booleannever converts to or from a number; writeflag ? 1 : 0, orcount != 0, when you need one.- Widening conversions (byte to short to int to long to float to double, and char to int) happen on their own; int or long to float and long to double can round.
- Narrowing needs a cast: integers keep their lowest bits, so the sign can change; floating-point values are cut toward zero and clamped to the range, and NaN becomes 0.
- Arithmetic promotes
byte,shortandchartoint, and compound assignment such as+=casts the result back, silently.
Terms in this lesson
- cast
- An explicit conversion written as (type) value, such as (byte) 200 or (int) 3.99. A cast between primitive types never throws, even when the value changes: (byte) 200 is -56.
- compound assignment
- An assignment operator such as +=, -= or *=: b += x means b = (T) (b + x), where T is the type of b, so the result is cast back to the variable's type, and narrowed without a warning.
- narrowing conversion
- A conversion to a type that may not hold the value, such as int to byte or double to int. It needs a cast: integers keep their lowest bits, and floating-point values are cut toward zero and clamped to the range.
- numeric promotion
- The rule that converts the operands of an arithmetic operator to a common type before it computes: byte, short and char become at least int, so the sum of two bytes is an int, and an int and a long give a long.
- surrogate pair
- Two UTF-16 code units, a high one from D800 to DBFF and a low one from DC00 to DFFF, that together encode one character beyond U+FFFF, such as an emoji. length() counts both; codePointCount counts one.
- UTF-16 code unit
- One 16-bit number of the UTF-16 encoding of text, the unit a Java char holds and String.length() counts. Most characters are one code unit; characters beyond the first 65,536, such as many emoji, take two.
- widening conversion
- A conversion to a type that holds every value of the old one, such as int to long or char to int, which Java applies on its own. Between integer types it never changes the value; int or long to float and long to double can round.
References
- The Java Language Specification, Java SE 25: 5.1.2 Widening Primitive Conversion (Oracle)
- The Java Language Specification, Java SE 25: 5.1.3 Narrowing Primitive Conversion (Oracle)
- The Java Language Specification, Java SE 25: 5.6 Numeric Contexts (Oracle)
- The Java Language Specification, Java SE 25: 15.26.2 Compound Assignment Operators (Oracle)
- The Java Language Specification, Java SE 25: 4.2.5 The boolean Type and boolean Values (Oracle)
- Character (Java SE 25 API) (Oracle)
- Boolean.parseBoolean (Java SE 25 API) (Oracle)
- JEP 400: UTF-8 by Default (OpenJDK)
- The javac Command (Java SE 25): the -Xlint key lossy-conversions (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress