Variables in Java — primitive types, String, final and casting, with Laura’s card (and the division that always comes out one short)

In Java syntax we left Laura’s enrolment card working, but with a flaw I had already warned you about: the 24 credits and the 15.50 price were typed by hand in five places. Something similar happened to me in my first Java lab: I changed the credits from 24 to 30 to try it out, forgot one of the five lines, and the receipt charged 30 credits at the price of 24. The program compiled, ran and lied.

Variables fix exactly that: each piece of data is written once, with a name, and the rest of the program uses it by that name. But in Java a variable isn’t just a name like in Python: it also has a fixed type, and the three most famous traps of the topic depend on that type: the division that comes out one short, the int that wraps round and the == that says false for two identical texts. We are going to trigger all of them with real output (Java 21, shown as it looks on a computer set to English), so that when they turn up in the lab you have already seen them.

Why Java makes you declare the type of every variable

In Python a variable is a label you can stick on anything, and move whenever you like:

credits = 24              # now it's an int
credits = "twenty-four"   # now it's a str: Python doesn't complain

Java has static typing: when you declare a variable you say what type of data it will hold, and that type never changes. If you try to put something else in it, the program doesn’t even compile:

public class FixedType {
    public static void main(String[] args) {
        int credits = 24;
        credits = "twenty-four";         // an int can't hold text
        credits = 24.5;                  // nor a decimal
    }
}

What javac says:

FixedType.java:4: error: incompatible types: String cannot be converted to int
        credits = "twenty-four";         // an int can't hold text
                  ^
FixedType.java:5: error: incompatible types: possible lossy conversion from double to int
        credits = 24.5;                  // nor a decimal
                  ^
2 errors

Notice that javac finds both errors before running anything. The first one is clear: a String can’t be converted into an int. The second is more interesting: possible lossy conversion from double to int. Putting 24.5 into an int would mean throwing away the .5, and Java won’t do that behind your back. You are going to see that sentence many times in this article.

Why so strict? Because Java compiles before running and wants to check at that moment that every operation makes sense. In Python a type error shows up when the program reaches that line (maybe in front of your teacher); in Java it shows up before the program exists. You type a bit more, but type mistakes never reach run time.

How to declare variables in Java

The pattern is always the same: type, name, equals, value and semicolon. This is Laura’s card with variables:

/** Laura's card with variables: every piece of data has a name, a type and a value (made-up data). */
public class VariablesCard {
    public static void main(String[] args) {
        String name = "Laura Santana Ruiz";   // text: double quotes
        char initial = 'L';                     // a single character: single quotes
        int year = 1;                           // integer
        int credits = 24;                       // integer
        double creditPrice = 15.50;             // decimal: in the code, always with a point
        boolean scholarship = false;            // only true or false

        System.out.println("Student: " + name + " (" + initial + ")");
        System.out.println("Year: " + year + "st");
        System.out.println("Credits: " + credits);
        System.out.println("Price per credit: " + creditPrice + " €");
        System.out.printf("Tuition fee: %.2f €%n", credits * creditPrice);
        System.out.println("Scholarship? " + scholarship);
    }
}

Output:

Student: Laura Santana Ruiz (L)
Year: 1st
Credits: 24
Price per credit: 15.5 €
Tuition fee: 372.00 €
Scholarship? false

Between the lines:

  • String name = "Laura Santana Ruiz";: the type goes before the name. String has a capital letter because it is a class (we’ll see what that means later).
  • char initial = 'L';: single quotes for one character. "L" with double quotes would be a String and wouldn’t compile.
  • double creditPrice = 15.50;: in the code, decimals always use a point, whatever country you are in.
  • Look at the output: println writes 15.5 (point and no trailing zero), while printf with %.2f writes the price with two decimals using the decimal separator of the computer’s language (a point in English; on a Spanish computer you’d see 372.00). It’s the same thing we saw in syntax: println uses Java’s own format and printf, your language’s.
  • credits * creditPrice multiplies an int by a double: Java turns 24 into 24.0 on its own and the result is double. That automatic conversion has a name (widening) and its own section further down.

If Laura now enrols for 30 credits, you change one line and the whole program knows. That’s what the syntax articles promised.

Declaring without a value

You can declare first and assign later (int credits; and then credits = 24;). What you can’t do is read it before giving it a value:

public class NoValue {
    public static void main(String[] args) {
        int credits;                      // declared, but with no value
        System.out.println(credits);      // what does it print?
    }
}
NoValue.java:4: error: variable credits might not have been initialized
        System.out.println(credits);      // what does it print?
                           ^
1 error

In Python, using a name that was never assigned gave NameError at run time. In Java, local variables (the ones inside main) have no default value and javac spots it when compiling: variable credits might not have been initialized.

Names: camelCase and no accents

The Java conventions followed in FP2 (and that some teachers mark):

WhatStyleExample
Variables and methodscamelCase, starts in lower casecreditPrice, averageGrade
ClassesPascalCase, starts in upper caseVariablesCard, String
Constants (final)CAPITALS with underscoresCREDIT_PRICE

Java accepts accented letters in names (año compiles), but it is better to avoid them: the day the file is opened with another encoding you’ll get strange errors.

You can declare several variables of the same type on one line: int midterm1 = 7, midterm2 = 8;. Only do it with data that belongs together; otherwise one per line reads better.

Primitive types: what they are and why they exist

Java has two families of types: primitives and objects. There are eight primitives, they are written in lower case and they store the value as it is, with nothing around it. They are the bricks everything else is built from:

TypeWhat it holdsBitsExample
bytevery small integer8byte b = 100;
shortsmall integer16short s = 30000;
intinteger (the usual one)32int credits = 24;
longbig integer64long record = 20260004321L;
floatsingle-precision decimal32float f = 7.25f;
doubledecimal (the usual one)64double grade = 7.25;
booleantrue or false—boolean scholarship = false;
charone character16char initial = 'L';

Wondering whether String is a primitive data type? No: it isn’t in the table. It’s a class, and that’s why it starts with a capital letter. We’ll get to it in its own section.

You don’t need to learn the limits by heart: Java’s own classes give them to you. This program prints them:

/** The limits of each integer and decimal type, taken from their wrapper classes. */
public class Ranges {
    public static void main(String[] args) {
        System.out.println("byte:   " + Byte.MIN_VALUE + " to " + Byte.MAX_VALUE + " (" + Byte.SIZE + " bits)");
        System.out.println("short:  " + Short.MIN_VALUE + " to " + Short.MAX_VALUE + " (" + Short.SIZE + " bits)");
        System.out.println("int:    " + Integer.MIN_VALUE + " to " + Integer.MAX_VALUE + " (" + Integer.SIZE + " bits)");
        System.out.println("long:   " + Long.MIN_VALUE + " to " + Long.MAX_VALUE + " (" + Long.SIZE + " bits)");
        System.out.println("float:  up to " + Float.MAX_VALUE + " (" + Float.SIZE + " bits)");
        System.out.println("double: up to " + Double.MAX_VALUE + " (" + Double.SIZE + " bits)");
        System.out.println("char:   " + (int) Character.MIN_VALUE + " to " + (int) Character.MAX_VALUE + " (" + Character.SIZE + " bits)");
    }
}
byte:   -128 to 127 (8 bits)
short:  -32768 to 32767 (16 bits)
int:    -2147483648 to 2147483647 (32 bits)
long:   -9223372036854775808 to 9223372036854775807 (64 bits)
float:  up to 3.4028235E38 (32 bits)
double: up to 1.7976931348623157E308 (64 bits)
char:   0 to 65535 (16 bits)

3.4028235E38 is scientific notation: 3.4 times 10 to the 38th. Decimals go much further than integers, but in exchange they aren’t exact (you’ll see it in the exercises with cents). And char goes from 0 to 65535: it is an unsigned number, which explains why you can add to it.

The integer types: byte, short, int and long

All four do the same job: they store numbers with no decimals. The only difference is how much space they take and, therefore, how far they reach. An int goes up to 2147483647 (about 2.1 billion); a long, to over 9 quintillion.

When to use each one

  • int for almost everything: credits, whole grades, counters, years. It’s the type Java assumes when you write a whole number.
  • long when the number can go past 2 billion: milliseconds, a record number with the year in front, world population.
  • byte and short only to save memory with huge amounts of data. You’ll hardly see them in FP2.

Literals: the L for long and the f for float

A number written directly in the code (a literal) also has a type: 24 is int and 7.25 is double. That has a consequence that catches everybody out:

long worldPopulation = 8100000000;   // no L: Java reads it as int
float averageGrade = 7.25;           // no f: Java reads it as double
Literals.java:3: error: integer number too large
        long worldPopulation = 8100000000;   // no L: Java reads it as int
                               ^
1 error

integer number too large: even though the variable is long, the number 8100000000 is read first as an int, and it doesn’t fit. javac only shows that error because it’s one of those that stops it reading the file; once you fix it, in the next round it will complain about the float (a double in a float also loses precision). The fix is to tell Java the type of the literal with a suffix:

/** The same data with their suffix: L for long and f for float. */
public class LiteralsFixed {
    public static void main(String[] args) {
        long worldPopulation = 8100000000L;  // L: this number is long
        float averageGrade = 7.25f;          // f: this number is float
        double gradeDouble = 7.25;           // with no suffix, a decimal is double
        System.out.println(worldPopulation);
        System.out.println(averageGrade);
        System.out.println(gradeDouble);
    }
}
8100000000
7.25
7.25

The L can be lower case, but 8100000000l looks like it ends in a 1. Always write it in upper case.

The decimal types: float and double

float keeps about 7 significant digits and double, about 15. A literal with a point is double unless it has an f (or F).

When to use float and when double

In FP2, always double. float exists for graphics, sensors or huge amounts of data where memory matters. Neither of them is any good for exact money: in the exercises you’ll see why the print shop inventory adds up to 6149.599999999999 and how cents in a long fix it.

boolean: only true or false

In Python it was True and False with a capital letter; in Java, true and false in lower case. And there’s a more important difference: in Java a number is not a boolean. In Python if 1: worked; in Java, a condition has to be a real boolean, such as credits > 0. You’ll see it in flow control.

Overflow: the int that wraps round

What happens if an int goes past its maximum? In Python integers grow as much as they need to. In Java they don’t: the value wraps round and comes out at the other end. No error, no warning:

/** What happens when an int goes past its maximum, and how long avoids it. */
public class Overflow {
    public static void main(String[] args) {
        int maximum = Integer.MAX_VALUE;
        System.out.println("Maximum int:    " + maximum);
        System.out.println("Maximum + 1:    " + (maximum + 1));      // wraps round
        System.out.println("With long:      " + (maximum + 1L));     // 1L turns the sum into long

        // Milliseconds in a 365-day course: the sum is done in int and overflows
        int msCourseWrong = 365 * 24 * 60 * 60 * 1000;
        long msCourseLate = 365 * 24 * 60 * 60 * 1000;                 // long on the left is not enough
        long msCourseRight = 365L * 24 * 60 * 60 * 1000;               // the L on the FIRST number
        System.out.println("ms in a course (int):         " + msCourseWrong);
        System.out.println("ms in a course (long, late):  " + msCourseLate);
        System.out.println("ms in a course (365L):        " + msCourseRight);
    }
}
Maximum int:    2147483647
Maximum + 1:    -2147483648
With long:      2147483648
ms in a course (int):         1471228928
ms in a course (long, late):  1471228928
ms in a course (365L):        31536000000
The int that wraps roundAn int goes from -2147483648 to 2147483647. Adding 1 to the maximum jumps to the minimum without any error. 365 * 24 * 60 * 60 * 1000 is worked out in int and gives 1471228928; with 365L it is worked out in long and gives 31536000000.-214748364802147483647Everything that fits in an int (32 bits)MAX_VALUE + 1: no error, it wraps round to the minimumMilliseconds in a course:365 * 24 * 60 * 60 * 1000 → 1471228928365L * 24 * 60 * 60 * 1000 → 31536000000The sum uses the type of the numbers, not the type of the variable
Overflow is silent: the program carries on with a wrong number.

Three lessons in one output:

  1. maximum + 1 gives -2147483648: the minimum. Picture the odometer of an old car going from 99999 to 00000.
  2. maximum + 1L gives the right number: the L turns that sum into a long sum.
  3. The sneakiest one: long msCourseLate = 365 * 24 * 60 * 60 * 1000; also gives 1471228928. The variable is long, but the sum on the right is worked out entirely in int (all the numbers are int) and overflows before it reaches the variable. Checking by hand: 365 × 24 × 60 × 60 × 1000 = 31,536,000,000, which is more than 2,147,483,647. The fix is for the first number to be long: 365L * 24 * ….

Thinking it’s enough for the variable on the left to be long (or double). Java first works out the expression on the right with the types of its numbers and only then stores the result. If the overflow has already happened, the variable gets the wrong number.

char: a number shaped like a letter

A char holds one Unicode character, but underneath it’s a 16-bit number: the character’s code in the table. The first 128 match ASCII (the good old 7-bit table, which fits in a byte): 'A' is 65, 'a' is 97, '0' is 48 and the space is 32. Since it’s a number, you can convert it and do arithmetic with it:

/** A char is a number shaped like a letter: the Unicode table (the first 128, ASCII). */
public class Characters {
    public static void main(String[] args) {
        char letter = 'L';
        int code = letter;                              // char → int: automatic
        System.out.println(letter + " has the code " + code);
        System.out.println("'L' + 1 = " + (letter + 1));         // char + int = int
        System.out.println("(char) ('L' + 1) = " + (char) (letter + 1));
        System.out.println("'A' + 'B' = " + ('A' + 'B'));        // two chars are added as numbers
        System.out.println("\"\" + 'A' + 'B' = " + ("" + 'A' + 'B'));  // with text in front, they stick together
        letter++;                                       // ++ does work with char
        System.out.println("After letter++: " + letter);
        letter += 2;                                    // += too (it has a hidden cast)
        System.out.println("After letter += 2: " + letter);
        char digit = '7';
        System.out.println("'7' as a number: " + (int) digit + ", its value: " + (digit - '0'));
    }
}
L has the code 76
'L' + 1 = 77
(char) ('L' + 1) = M
'A' + 'B' = 131
"" + 'A' + 'B' = AB
After letter++: M
After letter += 2: O
'7' as a number: 55, its value: 7

Line by line:

  • int code = letter; works without a cast: a char fits in an int. 'L' is 76.
  • letter + 1 gives 77, not M: char + int gives int. To get back to a letter you need the cast (char) (letter + 1).
  • 'A' + 'B' gives 131 (65 + 66): two char values are added. However, "" + 'A' + 'B' gives AB because, as we saw in syntax, concatenation goes left to right and starting with a text turns everything into text.
  • letter++ and letter += 2 do work: those operators carry a hidden cast back to char.
  • digit - '0' turns the character '7' into the number 7 (55 − 48 = 7). It’s the classic trick for turning text digits into numbers one at a time.

What does not compile is the long version of letter++:

public class CharPlus {
    public static void main(String[] args) {
        char letter = 'L';
        letter = letter + 1;                 // char + int gives int
    }
}
CharPlus.java:4: error: incompatible types: possible lossy conversion from int to char
        letter = letter + 1;                 // char + int gives int
                        ^
1 error

letter + 1 is an int, and putting an int into a char can lose data (an int goes much further than 65535). possible lossy conversion again.

Single quotes for char ('L') and double quotes for String ("L"). In Python it didn’t matter; in Java they are different types and char c = "L"; doesn’t compile.

String: the one that isn’t primitive

String isn’t one of the eight primitives: it’s a class that comes with Java, and every text is an object of that class. That’s why it has methods (called with a dot) and why it starts with a capital letter:

/** String isn't primitive: it is an object with methods, and it can't be changed (immutable). */
public class Texts {
    public static void main(String[] args) {
        String name = "Laura";
        String surname = "Santana";
        String fullName = name + " " + surname;        // creates a new String
        System.out.println(fullName + " has " + fullName.length() + " characters");
        System.out.println(fullName.toUpperCase());
        System.out.println("Initial: " + fullName.charAt(0));
        fullName.toUpperCase();                            // doesn't change fullName: returns another one
        System.out.println(fullName);
        String empty = "";
        System.out.println("Length of \"\": " + empty.length());
    }
}
Laura Santana has 13 characters
LAURA SANTANA
Initial: L
Laura Santana
Length of "": 0

Two details in the output:

  • fullName.length() counts 13 characters: «Laura» (5) + the space (1) + «Santana» (7). With brackets, because it’s a method.
  • The line fullName.toUpperCase(); on its own changes nothing: the next print still says Laura Santana. String objects are immutable, just like in Python: toUpperCase() returns a new upper-case text, and if you don’t store it in a variable (fullName = fullName.toUpperCase();), it’s lost.

String methods (substring, indexOf, split, replace…) have their own topic later on. For this one, length(), charAt(), toUpperCase() and toLowerCase() are enough.

Primitives versus objects: the stack and the heap

Here is the difference that explains nearly everything odd in this topic. Java splits memory into two areas:

  • The stack: the variables of each method. A primitive variable keeps its value here, inside its own box.
  • The heap: objects. A variable of an object type (a String, for example) doesn’t keep the text: it keeps a reference, an arrow pointing to the object on the heap.
Primitives on the stack, objects on the heapcredits and copy keep their number inside the stack. a, b and c keep an arrow: a and b point to the same "lsantana" in the String pool; c points to another object created with new. That is why a == b is true, a == c is false and a.equals(c) is true.Stack: variables of mainHeap: objectsint credits24int copy30String aString bString c"lsantana"from the String pool"lsantana"created with newa == b → truesame arrowa == c → falseanother objecta.equals(c) → truesame text
A primitive variable is a box holding the value; a String variable is a box holding an arrow. == compares the arrows.

That’s why copying behaves differently depending on the type. int copy = credits; copies the number: if you then change copy, credits is still 24. String b = a; copies the arrow: a and b point to the same object. With String objects you don’t notice because they are immutable (nobody can change the object both of them point to), but with the objects you’ll create in Classes and objects you will.

The difference between == and .equals(): Java’s classic trap

== compares what is inside the box. With primitives that’s the value, so 7 == 7 is true. With objects, what’s in the box is the arrow: == asks «do you point to the same object?», not «do you say the same thing?». To compare the content there’s equals():

/** == compares references; equals() compares the content. */
public class Equality {
    public static void main(String[] args) {
        String a = "lsantana";                 // literal: goes to the String pool
        String b = "lsantana";                 // the same literal: the same object from the pool
        String c = "l" + "santana";            // two literals: Java joins them when compiling
        String initial = "l";
        String d = initial + "santana";        // worked out at run time: new object
        String e = new String("lsantana");     // new: always a new object

        System.out.println("a == b      " + (a == b));
        System.out.println("a == c      " + (a == c));
        System.out.println("a == d      " + (a == d));
        System.out.println("a == e      " + (a == e));
        System.out.println("a.equals(d) " + a.equals(d));
        System.out.println("a.equals(e) " + a.equals(e));

        Integer x = 127, y = 127;              // Integer keeps -128 to 127 in a cache
        Integer z = 128, w = 128;
        System.out.println("127 == 127  " + (x == y));
        System.out.println("128 == 128  " + (z == w));
        System.out.println("equals 128  " + z.equals(w));
    }
}
a == b      true
a == c      true
a == d      false
a == e      false
a.equals(d) true
a.equals(e) true
127 == 127  true
128 == 128  false
equals 128  true

Case by case:

ComparisonResultWhy
a == btrueTwo identical literals: Java keeps a single one in the String pool and both point to it
a == ctrue"l" + "santana" are two literals: javac joins them when compiling and the same literal remains
a == dfalseinitial + "santana" uses a variable: it’s worked out at run time and creates a new object
a == efalsenew String(...) always creates a new object
a.equals(d) and a.equals(e)trueequals() compares the letters, one by one

The danger is that == sometimes gets it right (the first two rows). You test with literals, it works and you relax. Then the text comes from a Scanner, from a file or from concatenating variables, and it stops working. In the practice you’ll see it with Laura’s card username.

Something similar happens with Integer: Java keeps the objects from -128 to 127 in a cache, so 127 == 127 gives true and 128 == 128 gives false. The rule is the same for all objects:

Comparing texts with ==. For String (and any object), always equals(): username.equals("lsantana"). == only for primitives.

final: Java’s constant

Some data mustn’t change in the whole program: the price per credit, the admin fee. final in front of the type turns the variable into a constant: it gets a value once and can’t be assigned again. By convention, in capitals:

/** final for what doesn't change and var so Java works out the type. */
public class Constants {
    public static void main(String[] args) {
        final double CREDIT_PRICE = 15.50;   // final: it can't be assigned again
        final int CREDITS = 24;
        final double FEE = 27.30;
        System.out.printf("Tuition: %d x %.2f + %.2f = %.2f €%n",
                CREDITS, CREDIT_PRICE, FEE, CREDITS * CREDIT_PRICE + FEE);

        var year = 1;                        // var: Java works out int
        var degree = "GCID";                 // works out String
        var average = 7.5;                   // works out double
        System.out.println(degree + ", " + year + "st, average " + average);
    }
}
Tuition: 24 x 15.50 + 27.30 = 399.30 €
GCID, 1st, average 7.5

Check: 24 × 15.50 = 372.00, plus 27.30 = 399.30 €. And if someone tries to change a constant:

public class BadFinal {
    public static void main(String[] args) {
        final double CREDIT_PRICE = 15.50;
        CREDIT_PRICE = 16.00;            // a price rise?
        var year = 1;
        year = "first";                  // var isn't dynamic
    }
}
BadFinal.java:4: error: cannot assign a value to final variable CREDIT_PRICE
        CREDIT_PRICE = 16.00;            // a price rise?
        ^
BadFinal.java:6: error: incompatible types: String cannot be converted to int
        year = "first";                  // var isn't dynamic
               ^
2 errors

The first error is the one we were after: cannot assign a value to final variable CREDIT_PRICE. In Python, constants in capitals were just a habit; in Java, final is enforced by the compiler. The second error belongs to the next section.

Multiple declarations and var

Since Java 10 there’s var: you write var instead of the type and Java works it out from the value (type inference). In the program above, var year = 1; makes year an int and var degree = "GCID";, a String.

But careful: var doesn’t turn Java into Python. The type is decided when you declare and stays fixed, as the second error of BadFinal shows: year is an int even though you wrote it with var, and it won’t accept "first". Also, var only works for local variables with a starting value (var x; doesn’t compile: there’s nothing to work the type out from).

Java has no typeof like JavaScript or type() like Python for primitives: you know the type because you wrote it (or because var worked it out from the value). FP2 notes and exams nearly always use the explicit type; var is valid, but better used when the type is obvious.

Converting between types: casting

There are two directions:

  • Widening: from a small type to a big one. It always fits, so Java does it on its own.
  • Narrowing: from a big type to a small one. Data can be lost, so Java demands that you ask for it with a cast: the type in brackets in front of the value, (int) grade.
The numeric type ladderFrom byte to double, each type fits in the one to its right and Java converts on its own; char goes into int. To the left you need a cast and data can be lost: (int) 9.99 gives 9, (int) 3000000000L gives -1294967296, (byte) 200 gives -56.To the right: automatic (widening)byte8 bitsshort16 bitsint32 bitslong64 bitsfloat32 bitsdouble64 bitschar'L' → 76To the left: only with a cast, and it may lose data (narrowing)(int) 9.99→ 9truncates(int) 3000000000L→ -1294967296(byte) 200→ -56200 – 256(char) 77→ 'M'code → letterint → float is automatic too, but 16777217 becomes 16777216
Going up the ladder is free; going down needs a cast and you answer for whatever is lost.
/** Automatic (widening) and forced (narrowing) conversions between types. */
public class Casting {
    public static void main(String[] args) {
        int credits = 24;
        long creditsLong = credits;          // int → long: automatic
        double creditsDouble = credits;      // int → double: automatic
        System.out.println(creditsLong + " / " + creditsDouble);

        double grade = 9.99;
        System.out.println("(int) 9.99   = " + (int) grade);            // truncates
        System.out.println("(int) -9.99  = " + (int) -grade);           // towards zero too
        System.out.println("Math.round   = " + Math.round(grade));      // rounds (returns long)
        System.out.println("(int) 3000000000L = " + (int) 3000000000L); // doesn't fit: bits cut off
        System.out.println("(byte) 200   = " + (byte) 200);             // 200 - 256
        int big = 16777217;
        float f = big;                       // automatic... but it loses precision
        System.out.println("int " + big + " → float " + (int) f);
    }
}
24 / 24.0
(int) 9.99   = 9
(int) -9.99  = -9
Math.round   = 10
(int) 3000000000L = -1294967296
(byte) 200   = -56
int 16777217 → float 16777216

What matters in each line:

  • (int) 9.99 gives 9 and (int) -9.99, -9: the cast truncates (drops the decimals and moves towards zero), it doesn’t round. To round, Math.round(9.99) gives 10 (and returns a long).
  • (int) 3000000000L gives -1294967296: the number doesn’t fit and Java keeps the bottom 32 bits. With a cast, Java obeys you even if the result is absurd: you answer for it.
  • (byte) 200 gives -56: a byte goes up to 127, and 200 − 256 = −56.
  • int → float is automatic, but 16777217 becomes 16777216: a float doesn’t have enough digits for such a long number. Automatic doesn’t always mean lossless.

Without the cast, javac won’t let the narrowing through:

public class NoCast {
    public static void main(String[] args) {
        double grade = 9.99;
        int wholeGrade = grade;             // double → int with no cast
    }
}
NoCast.java:4: error: incompatible types: possible lossy conversion from double to int
        int wholeGrade = grade;             // double → int with no cast
                         ^
1 error

Integer division

And now the trap that gives this article its name. When you divide two int values, Java does integer division: the result is another int and the decimals are thrown away. With Laura’s grades:

/** Integer division and where to put the cast, with Laura's grades (made-up data). */
public class Division {
    public static void main(String[] args) {
        int midterm1 = 7;
        int midterm2 = 8;
        int practical = 6;
        int sum = midterm1 + midterm2 + practical;                   // 21

        System.out.println("7 / 2 = " + (7 / 2));                    // int / int = int
        System.out.println("7 % 2 = " + (7 % 2));                    // the remainder
        System.out.println("7 / 2.0 = " + (7 / 2.0));                // with a double, real division
        System.out.println("Average int:        " + sum / 3);        // 21 / 3 = 7 (exact, by luck)
        System.out.println("Average of 7 and 8: " + (midterm1 + midterm2) / 2);            // 15 / 2 = 7
        System.out.println("Cast before:        " + (double) (midterm1 + midterm2) / 2);   // 15.0 / 2 = 7.5
        System.out.println("Cast after:         " + (double) ((midterm1 + midterm2) / 2)); // (double) 7 = 7.0
        System.out.println("With 2.0:           " + (midterm1 + midterm2) / 2.0);          // 7.5
        double averageWrong = (midterm1 + midterm2) / 2;             // the double arrives too late
        System.out.println("double = int / int: " + averageWrong);
        System.out.println("1 / 3 * 3 = " + (1 / 3 * 3));            // 0 * 3
    }
}
7 / 2 = 3
7 % 2 = 1
7 / 2.0 = 3.5
Average int:        7
Average of 7 and 8: 7
Cast before:        7.5
Cast after:         7.0
With 2.0:           7.5
double = int / int: 7.0
1 / 3 * 3 = 0
  • 7 / 2 gives 3 and 7 % 2 gives 1: quotient and remainder, like in primary school. Very useful together (you’ll use them in the practice to split instalments).
  • The average of 7, 8 and 6 gives 7 because 21 / 3 is exact: integer division didn’t show… by luck.
  • The average of 7 and 8 gives 7 instead of 7.5: 15 / 2 = 7.
  • Cast before: (double) (midterm1 + midterm2) / 2 turns 15 into 15.0 and then divides: 7.5.
  • Cast after: (double) ((midterm1 + midterm2) / 2) divides first (7) and then converts: 7.0. The cast arrives too late.
  • double averageWrong = (midterm1 + midterm2) / 2; gives 7.0 for the same reason as the overflow long: the variable is double, but the division was done between two int values.
  • 1 / 3 * 3 gives 0: 1 / 3 = 0, and 0 × 3 = 0.

Writing the average as (a + b) / 2 with two int values. The rule that never fails: as soon as there’s a division and you want decimals, make one of the two operands a double before dividing: / 2.0 or a cast in front of the dividend.

Converting between String and numbers

A text "24" isn’t the number 24 and can’t be converted with a cast ((int) "24" doesn’t compile: a String is an object, not a number). There are methods for that:

/** From text to number and from number to text: it isn't a cast, they are methods. */
public class Convert {
    public static void main(String[] args) {
        String creditsTxt = "24";                      // this is how data arrives from a form or a file
        String priceTxt = "15.50";
        int credits = Integer.parseInt(creditsTxt);
        double price = Double.parseDouble(priceTxt);
        System.out.println(creditsTxt + creditsTxt);   // two texts: they stick together
        System.out.println(credits + credits);         // two ints: they are added
        System.out.println(credits * price);
        String total = String.valueOf(credits * price);
        System.out.println("As text: " + total + " (" + total.length() + " characters)");
        System.out.println("With \"\" + : " + ("" + 42));
    }
}
2424
48
372.0
As text: 372.0 (5 characters)
With "" + : 42

creditsTxt + creditsTxt sticks two texts together (2424); credits + credits, already converted, adds (48). The other way round, number to text: String.valueOf(...) or the trick "" + number.

And if the text isn’t shaped like a number? Something very typical in Spain happens: writing the price with a comma.

public class DecimalComma {
    public static void main(String[] args) {
        String priceTxt = "15,50";                      // written the Spanish way
        double price = Double.parseDouble(priceTxt);
        System.out.println(price);
    }
}
Exception in thread "main" java.lang.NumberFormatException: For input string: "15,50"
        at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
        at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.base/java.lang.Double.parseDouble(Double.java:792)
        at DecimalComma.main(DecimalComma.java:4)

Double.parseDouble only understands the point, even on a computer set to Spanish. The program stops with a NumberFormatException and the last line of the trace (DecimalComma.java:4) tells you where. How to handle these exceptions without the program stopping is in the Exceptions topic; with the Scanner of the next topic the opposite happens (on a Spanish computer it expects the comma), and you’ll see it in its theory.

Wrapper classes: Integer, Double, Boolean…

Every primitive has a «wrapper» class that turns it into an object: int → Integer, double → Double, char → Character, boolean → Boolean, long → Long… You’ve already used their constants (Integer.MAX_VALUE) and their methods (Integer.parseInt). You need them above all for collections (ArrayList<Integer>), which only hold objects. Java converts from one to the other by itself (autoboxing and unboxing), but they are objects: they are compared with equals(), as you saw with 128.

Common mistakes with variables

  • Integer division by accident: (a + b) / 2 with two int values. A double operand before dividing.
  • Cast after the operation: (double) (a / b) arrives too late. The cast goes in front of the dividend.
  • Silent overflow: 365 * 24 * 60 * 60 * 1000 in int. The first number with L.
  • long without L or float without f: 8100000000 doesn’t compile as a literal; 7.25 is double.
  • == with texts: always equals() for String.
  • Thinking the cast rounds: (int) 9.99 is 9. To round, Math.round.
  • Commas in the code: 15,50 isn’t a number inside Java. In the code, a point; the comma only appears in printf output on a Spanish computer.

Watch it step by step in Python Tutor

pythontutor.com runs the code line by line and draws the variables and the objects with their arrows. Choose Java in the drop-down and paste this program (a single public class with main and no keyboard input):

public class References {
    public static void main(String[] args) {
        int credits = 24;
        int copy = credits;              // copies the value
        copy = 30;                       // credits is still 24

        String a = "lsantana";
        String b = a;                    // copies the reference
        String c = new String("lsantana"); // another object with the same text

        boolean sameRef = (a == c);
        boolean sameText = a.equals(c);
        System.out.println(credits + " " + copy);
        System.out.println((a == b) + " " + sameRef + " " + sameText);
    }
}

Output:

24 30
true false true

What to watch step by step:

  1. After int copy = credits; you’ll see two boxes with 24. When copy = 30; runs, only one changes: primitives are copied by value.
  2. With String b = a;, Python Tutor draws (if in its options you choose to show String objects instead of just their value) two arrows to the same object. It’s the stack and heap picture above.
  3. new String("lsantana") creates a second object on the heap, with the same text and a different address.
  4. On the last line, a == b gives true (same arrow), sameRef is false and sameText is true: compare it with the output above.

Quick summary

I want…I writeWatch out for…
A whole numberint credits = 24;Over 2147483647: long and L
A decimaldouble grade = 7.25;Point in the code; not exact for money
A characterchar initial = 'L';Single quotes; it’s a number (76)
A textString name = "Laura";It’s an object: equals(), never ==
A constantfinal double CREDIT_PRICE = 15.50;Can’t be reassigned
An average with decimals(a + b) / 2.0(a + b) / 2 with int truncates
Drop decimals(int) 9.99 → 9Truncates, doesn’t round: Math.round
Text → numberInteger.parseInt("24")a text like “15,50” throws NumberFormatException

Three ideas to keep:

  1. The type rules the sums: Java calculates with the type of the operands and only at the end stores the result in the variable. That’s why integer division and overflow go wrong even if the variable is double or long.
  2. Primitives in the box, objects behind an arrow: == compares what’s in the box; for texts, equals().
  3. Going up a type is free; going down needs a cast and can lose data.

Now it’s time to use all this in complete programs: in the Java variables practice we build Laura’s tuition receipt with instalments in cents, her final FP2 grade (with integer division hiding twice) and her university card, with a record number that doesn’t fit in an int. And then, in Java flow control, these variables start making decisions.

Next step: practiceJava variables practice — Laura’s receipt, final grade and student card (with the grade that came out 0.0)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *