Skip to content
Chapter 05 · Core Java

Variables, Primitive Types, and Type Casting ​

A program is mostly about data: a price, an age, a name, a yes or no answer. Before you can do anything with data, you need somewhere to keep it. That place is a variable.

What a variable is ​

A variable is a named box that holds one value. In Java, every box also has a fixed type that says what kind of value may go inside.

java
int age = 25;

This line does three things. int is the type (whole numbers). age is the name. 25 is the value you put in. From now on, writing age in your code means "the value currently inside that box".

Statically typed and strongly typed ​

You will hear Java described with two terms. They mean different things.

Statically typed: every variable's type is fixed when you write the code, and the compiler checks it before the program runs.

java
int x = 10;
x = "hello";   // compile error: a String does not fit in an int box

Strongly typed: each type has a fixed range of values, and Java restricts what you can assign. A value that does not fit the type is rejected unless you convert it explicitly.

java
byte small = 200;   // compile error: possible lossy conversion from int to byte (200 > 127)

Naming rules ​

The compiler enforces a few rules: names are case sensitive (count and Count are different), may contain Unicode letters, digits, $ and _, must start with a letter, $ or _ (never a digit), and cannot be a reserved word such as int, class or new.

On top of that, Java developers follow conventions:

java
int studentCount = 3;                 // variables and methods: camelCase
static final int MAX_SPEED = 200;     // constants: UPPER_SNAKE_CASE

The eight primitive types ​

Java has exactly eight primitive types. A primitive variable holds the raw value itself, not a reference to an object.

TypeSizeRangeDefault (for fields)
byte8 bits-128 to 1270
short16 bits-32,768 to 32,7670
int32 bitsabout -2.1 billion to 2.1 billion (-2^31 to 2^31 - 1)0
long64 bits-2^63 to 2^63 - 1 (about ±9.2 × 10^18)0L
float32 bitsabout ±3.4 × 10^38, ~7 significant digits0.0f
double64 bitsabout ±1.8 × 10^308, ~15 to 16 significant digits0.0
char16 bits0 to 65,535 (unsigned)'\u0000'
booleannot specified by the JVM spectrue or falsefalse

byte, short, int, long and char hold whole numbers. float and double hold fractional numbers. boolean holds only true or false.

Which one should you use? int for whole numbers and double for fractional numbers, unless you have a reason not to.

char ​

A char holds one UTF-16 code unit, which is a number from 0 to 65,535. You can assign a character or its number:

java
char a = 'A';
char b = 65;     // also 'A'

char is Java's only unsigned numeric type. Codes 0 to 127 are the same as ASCII (65 is 'A', 97 is 'a').

long and the L suffix ​

A whole number written in code (a literal) is an int by default. If it is too big for int, add L:

java
long worldPopulation = 8_100_000_000L;  // underscores are only for readability
long bad = 8_100_000_000;               // compile error: integer number too large

Use uppercase L; lowercase l looks like the digit 1.

float and the f suffix ​

A fractional literal such as 63.2 is a double by default. To store it in a float, add f. For a double you may add d, but it is optional:

java
float f = 63.2f;    // OK
double d = 63.2d;   // OK, same as 63.2
float g = 63.2;     // compile error: possible lossy conversion from double to float

How negative numbers are stored: two's complement ​

Computers store integers as bits. Java uses two's complement for negative numbers. To negate a number: flip every bit, then add 1. With 4 bits:

 3  = 0011
flip  1100
+1    1101   = -3

Check it: 0011 + 1101 = 10000. The fifth bit does not fit in 4 bits and falls off, leaving 0000. So 3 + (-3) = 0, using the same adder circuit as ordinary addition.

The leftmost bit acts as the sign: 0 means zero or positive, 1 means negative. 4 bits therefore cover -8 (1000) to 7 (0111). A byte has 8 bits, so it covers -128 (1000 0000) to 127 (0111 1111). The range has one more negative number because zero uses one of the "sign bit 0" patterns.

Overflow wraps around silently ​

If a result does not fit, Java does not throw an error. It keeps the low bits and wraps around, like an odometer rolling over.

java
byte b = 127;
b++;
System.out.println(b);          // -128

System.out.println((byte) 130); // -126
System.out.println((byte) 256); // 0

Never use float or double for money ​

float and double store numbers in binary, and most decimal fractions (such as 0.1) have no exact binary form. You get the nearest approximation:

java
float a = 0.3f;
float b = 0.1f;
System.out.println(a - b);   // 0.20000002, not 0.2

The same happens with double.

For money, use BigDecimal, created from a String:

java
import java.math.BigDecimal;

BigDecimal price = new BigDecimal("0.10");
BigDecimal tax   = new BigDecimal("0.20");
System.out.println(price.add(tax));   // 0.30, exactly

The next chapter shows exactly how floating point numbers are stored and why this happens.

Converting between types ​

Widening: automatic ​

Moving a value into a bigger type happens automatically:

java
int score = 95;
long big = score;     // automatic
double d = score;     // automatic

The smaller type always fits into the bigger one, so no cast is needed.

Narrowing: you must cast ​

Going into a smaller type can lose data, so Java requires an explicit cast:

java
int i = 10;
byte b = (byte) i;           // 10: fits, no problem

int big = 128;
byte wrapped = (byte) big;   // -128: past 127 it wraps around to the start of the range
byte next = (byte) 129;      // -127

Arithmetic promotes small types to int ​

In any arithmetic expression, byte, short and char values are promoted to int first. So the result of adding two bytes is an int:

java
byte a = 10, b = 20;
byte sum = a + b;            // compile error: a + b is an int
byte ok  = (byte) (a + b);   // cast the whole expression
int  sum2 = a + b;           // or store it in an int

When an expression mixes types, the whole expression is promoted to the largest type in it:

java
int i = 34;
double d = 20.0;
double r = i + d;            // 54.0
int bad = i + d;             // compile error: the expression is a double
int ok = (int) (i + d);      // 54

Where variables live, and whether they start with a value ​

Where you declare a variable decides its kind:

java
public class Demo {
    int count;                 // instance variable: one per object, starts at 0
    static int total;          // static variable: one per class, starts at 0

    Demo(int start) {          // constructor parameter
        count = start;
    }

    void add(int amount) {     // method parameter
        int doubled = amount * 2;   // local variable: no default value
        count += doubled;
    }
}

The rule to remember: fields get default values, local variables do not. Reading a local variable before assigning it is a compile error:

java
void check() {
    int x;
    System.out.println(x);     // compile error: variable x might not have been initialized
}

A static variable is shared by every object of the class. Change it through one object and all of them see the new value, so access it through the class name: Demo.total.

Check yourself ​

  1. What is the difference between statically typed and strongly typed?
  2. Why is the range of byte -128 to 127?
  3. What does (byte) 128 produce, and why?
  4. Why does byte c = a + b; fail to compile when a and b are bytes?
  5. Which variables get default values: member, static, or local?
  6. Why should you not use float or double for money?
Answers
  1. Statically typed: every variable declares a type, checked at compile time. Strongly typed: each type has a fixed range and Java restricts what you may assign to it.
  2. Two's complement: 8 bits give 256 patterns; 128 have sign bit 0 (zero and 1 to 127) and 128 have sign bit 1 (-1 to -128).
  3. -128. 128 does not fit in a byte, so the value wraps around past 127 to the start of the range.
  4. a + b is promoted to int, and an int cannot be stored in a byte without a cast.
  5. Member and static variables get defaults (0, false, null, '\u0000'). Local variables do not; you must assign them before use.
  6. Binary floating point cannot store most decimal fractions exactly, so results like 0.3f - 0.1f come out as 0.20000002. Use BigDecimal instead.