Skip to content

Variables, Primitive Types, and Type Casting

What a Variable Actually Is

Think of a variable as a container. You have a bottle that holds water, a box that holds shoes, a jar that holds coins. Each container is designed to hold a specific kind of thing. You would not try to pour water into a shoe box.

In Java, a variable is exactly that: a named container that holds a value. When you write this:

java
int where = 32;

You are doing three things at once. You are telling Java what kind of container this is (int means it holds whole numbers). You are giving the container a name (where). And you are putting an initial value into it (32). From that point on, the name where refers to that container, and whenever you use where in your code, Java reaches into the container and pulls out whatever value is stored there.

This container analogy matters because it sets up everything else in this article. Different container types have different sizes. Some containers are small and can only hold values within a tight range. Some are large and can hold enormous values. Some hold decimal numbers. Some just hold a yes or no answer. Understanding containers helps you understand why the rules exist.


Statically Typed and Strongly Typed

Java is both statically typed and strongly typed. Interviewers love to ask about this. These are two different things, and you need to be able to explain both.

Statically typed means every variable must declare its type at the moment you create it, and the compiler knows the type of every expression before the program ever runs. The type is locked in at compile time. You cannot write:

java
int x = 10;
x = "hello";   // COMPILE ERROR: incompatible types

Once you declare x as an int, it stays an int forever. The compiler refuses to let you put a String into an int container. This is static typing: types are checked at compile time, not at runtime.

Strongly typed means each type has a hard range and you cannot silently cross type boundaries. Java enforces that the value you put into a container is appropriate for that container. You cannot assign a double to an int without explicitly telling the compiler you accept the consequences:

java
double price = 99.99;
int dollars = price;         // COMPILE ERROR: possible lossy conversion from double to int
int dollars = (int) price;   // OK: you explicitly accept the risk
// dollars is now 99, not 100 — the decimal part is dropped, not rounded

Together, these two properties let the compiler catch an enormous class of bugs before your program ever runs. You might think of them as restrictions, but they are gifts. Every type error the compiler catches is a runtime crash you will never have to debug.


Naming Your Variables

Java has a set of rules and conventions for naming variables. Some of these are enforced by the compiler. Others are just conventions that every Java developer follows.

The hard rules are: variable names are case sensitive (so count and Count are completely different variables), names can contain letters, digits, the dollar sign, and the underscore, names cannot start with a digit, and names cannot be Java reserved words like int, class, for, while, new, or static.

These are all valid names:

java
int age = 25;
int _tempValue = 0;
int $special = 10;

This is not valid because it starts with a digit:

java
int 9lives = 9;   // COMPILE ERROR

The conventions are: use camelCase for variables and methods (start lowercase, capitalize each new word). If a variable is only one word, keep it all lowercase. If it is two words, run them together with the second word capitalized:

java
int jaipur = 2;        // one word: all lowercase
int jaipurCity = 2;    // two words: camelCase

For constants, use all capital letters with underscores separating words. A constant is declared with static final and cannot be changed after it is set:

java
static final int MAX_SPEED = 200;   // constant: ALL_CAPS by convention

The Eight Primitive Types

Java has exactly eight primitive types. These are not objects. They are the raw building blocks of everything else. They hold binary values directly and have no object overhead: no header, no reference pointer, just the bits.

TypeSizeRangeDefault value (member)Example literal
byte1 byte (8 bits)128 to 1270byte b = 100;
short2 bytes (16 bits)32768 to 327670short s = 1000;
int4 bytes (32 bits)about 2.1 billion (2^31 to 2^31 1)0int i = 42;
long8 bytes (64 bits)about 9.2 x 10^18 (2^63 to 2^63 1)0Llong l = 100L;
float4 bytes (32 bits)IEEE 754 single precision0.0ffloat f = 63.2f;
double8 bytes (64 bits)IEEE 754 double precision0.0ddouble d = 63.2;
char2 bytes (16 bits)0 to 65535 (unsigned)'\u0000'char c = 'A';
booleanJVM dependenttrue or falsefalseboolean flag = true;

Five of these (byte, short, int, long, and char) are integral types: they hold whole numbers. Two of them (float and double) are fractional types: they hold decimal numbers. One (boolean) holds only a true or false value.


Understanding char

char is two bytes (16 bits) and holds a Unicode code point. The range is 0 to 65535, which covers all the standard characters and symbols across many writing systems.

Here is the key thing about char: you can assign either a character literal or an integer to it. If you assign an integer, Java uses the Unicode code point table to figure out which character that number represents.

java
char c1 = 'A';     // the letter A
char c2 = 65;      // also the letter A — 65 is the code point for capital A
char c3 = 97;      // lowercase a — 97 is the code point for lowercase a
System.out.println(c1);   // prints: A
System.out.println(c2);   // prints: A
System.out.println(c3);   // prints: a

char is also the only unsigned primitive type in Java. All other numeric primitives are signed, meaning they can hold negative values. char cannot. Its range starts at 0, not at a negative number.

The default value of char when declared as a class member is '\u0000', which is the null character (code point zero). This is not the same as null in the object sense. It is simply the character at position zero in the Unicode table.


Understanding long and the L Suffix

long is 8 bytes, giving it the range from 2^63 to 2^63 1. You need long when your numbers exceed what int can hold. The population of Earth, for example, exceeds 7.9 billion, which is beyond int's limit of about 2.1 billion.

When you write a numeric literal in Java, the compiler assumes it is an int. If the number is too large to fit in an int, the compiler will reject it. To tell the compiler that the literal is a long, append the letter L (uppercase is preferred over lowercase l because lowercase l looks too much like the digit 1):

java
long population = 7_900_000_000L;   // L suffix required — exceeds int range
long small = 100L;                  // L suffix optional when value fits in int, but fine to add
long bad = 7_900_000_000;           // COMPILE ERROR: integer number too large

The underscore in 7_900_000_000 is just a readability aid. Java allows underscores inside numeric literals to make large numbers easier to read at a glance. The underscore has no effect on the value.


Understanding float and the F Suffix

float and double both store decimal numbers. The difference is precision and size. double uses 8 bytes and has roughly twice the precision of float.

When you write a decimal literal like 63.2, Java treats it as a double by default. If you want to assign it to a float, you must append the letter f (or F). Without it, you get a compile error because you would be assigning a double value to a float container, which is a narrowing conversion:

java
double d = 63.2;    // fine — 63.2 is a double literal
float f = 63.2f;    // fine — f suffix makes it a float literal
float bad = 63.2;   // COMPILE ERROR: possible lossy conversion from double to float

You can also write double d = 63.2d; with an explicit d suffix, though it is redundant since double is the default.


Two's Complement: How Java Stores Negative Numbers

This is one of the most important concepts in this article and a favorite interview question. Understanding two's complement explains why the ranges of byte, short, int, and long look the way they do, and it also explains overflow behavior.

Computers store everything in binary: sequences of 0s and 1s. Storing positive numbers is straightforward. The number 3 in 4 bits is 0011. The number 7 in 4 bits is 0111. But how do you store negative numbers?

Java uses two's complement representation. Here is how it works, using a 4 bit example to keep the math manageable.

To represent a positive number, write it out normally in binary:

+3 in 4 bits:   0011

To represent the negative version of that number, follow two steps. First, flip every bit (this is called the one's complement):

Flip all bits of 0011:   1100

Then add 1 to the result:

1100 + 0001 = 1101

So 1101 is the two's complement representation of 3. This is how negative 3 is stored in memory.

You can verify this makes sense. If you add +3 and 3 you should get zero:

  0011   (+3)
+ 1101   (-3)
------
 10000

The result is 10000, which is 5 bits. But in a 4 bit system, the fifth bit simply falls off. What you are left with is 0000, which is zero. So positive three plus negative three equals zero. The same binary addition circuit handles both positive and negative numbers with no special cases.

The Sign Bit and the MSB

In two's complement, the leftmost bit is called the most significant bit (MSB) and it acts as the sign bit. If it is 0, the number is positive. If it is 1, the number is negative.

Look at the full 4 bit range:

0000 = 0
0001 = 1
0010 = 2
0011 = 3
0100 = 4
0101 = 5
0110 = 6
0111 = 7      (largest positive: MSB is 0)
1000 = -8     (smallest negative: MSB is 1)
1001 = -7
1010 = -6
1011 = -5
1100 = -4
1101 = -3
1110 = -2
1111 = -1

So a 4 bit signed integer goes from 8 to 7. That is 16 values total (2^4 = 16). Notice that there is one more negative value than positive. That is because zero takes up one of the positive slots.

Why byte Goes from 128 to 127

A byte is 8 bits. The same pattern applies. The MSB is the sign bit. When all bits are zero, the number is zero. When the MSB is 0 and the rest are 1, you get the largest positive number: 0111 1111 = 127. When the MSB flips to 1 and everything else is 0, you get the smallest (most negative) number: 1000 0000 = 128.

If you add up 128 positive values (0 through 127) and 128 negative values (1 through 128), you get 256, which is exactly 2^8.

The range is therefore 128 to 127. Interviewers sometimes expect you to know why it is not symmetric, and the answer is that zero occupies a slot in the positive side.


Overflow: What Happens When You Exceed the Range

Java does not throw an exception when an integer overflows. It silently wraps around. This is a very common source of bugs.

Take a byte holding the value 127, which is the maximum:

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

In binary, 127 is 0111 1111. Adding 1 gives 1000 0000. In two's complement, 1000 0000 is 128. The bit pattern that would represent 128 as a positive number does not exist in a signed 8 bit type. Instead, it wraps around to the minimum: 128.

You can also observe this with explicit casts:

java
byte b1 = (byte) 128;   // -128 (one past the maximum wraps to minimum)
byte b2 = (byte) 129;   // -127
byte b3 = (byte) 256;   //  0   (256 in 8 bits with wraparound = 0)

Think of it like an old car odometer. When you hit 999999 miles and go one more, the odometer rolls over to 000000. Java integer arithmetic does the same thing, silently, with no warning.

If you need arithmetic that throws an exception instead of silently wrapping, use Math.addExact() or Math.multiplyExact(). For very large numbers where overflow must never happen, use BigInteger.


Never Use float or double for Money

Before moving on, a critical warning. float and double use IEEE 754 binary representation, and they cannot represent decimal fractions like 0.1 exactly. The number 0.1 in binary is an infinitely repeating fraction, similar to how 1/3 in decimal is 0.333... forever. The stored value is an approximation.

This means calculations can produce surprising results:

java
double a = 0.3;
double b = 0.1;
double result = a - b;
System.out.println(result);   // prints 0.20000000000000004, NOT 0.2

In any situation where decimal precision matters, particularly with currency, use java.math.BigDecimal and always pass the value as a String, not as a double:

java
import java.math.BigDecimal;

BigDecimal price = new BigDecimal("0.10");   // String constructor — exact
BigDecimal tax   = new BigDecimal("0.20");
System.out.println(price.add(tax));          // prints 0.30 — exact

Widening Conversion: Automatic Promotion to a Larger Type

When you move a value from a smaller container to a larger container, no data is lost. Java performs this conversion automatically, without requiring any special syntax from you. This is called widening conversion or implicit casting.

The widening hierarchy goes like this:

byte  -->  short  -->  int  -->  long  -->  float  -->  double

Any type on the left can be automatically widened to any type on the right:

java
byte x = 10;
int intVariable = x;   // automatic widening: byte to int, no cast needed
System.out.println(intVariable);   // prints 10

int score = 95;
long longScore = score;    // int to long: automatic
double d = score;          // int to double: automatic
float f = score;           // int to float: automatic

The reason this is safe is that the larger container always has room for anything the smaller container can hold. A long can hold any int value without losing a single bit.


Narrowing Conversion: Explicit Cast Required

Going the other direction, from a larger type to a smaller type, is called narrowing conversion or explicit casting. Data might be lost, so Java requires you to write an explicit cast to prove that you understand the risk:

java
double price = 19.99;
int truncated = (int) price;    // explicit cast required
System.out.println(truncated);  // prints 19 — decimal part is TRUNCATED, not rounded

The decimal part disappears completely. Java does not round. It truncates. If you were expecting 20, you would be surprised.

The syntax for a cast is the target type in parentheses before the value: (int) price. This is your written acknowledgment to the compiler that you know what you are doing.

java
long bigNumber = 130L;
byte b = (byte) bigNumber;
// 130 in 8-bit binary is 1000 0010
// In two's complement, 1000 0010 is -126
System.out.println(b);   // prints -126

When the value being narrowed does not fit in the target type, the extra bits are simply dropped. Only the lower bits that fit in the target type are kept. This can produce values that look completely unrelated to the original.


Integer Promotion in Arithmetic Expressions

This is the rule that catches nearly every Java beginner off guard, and it trips up experienced developers too.

The Java Language Specification says that byte, short, and char are automatically promoted to int whenever they appear in an arithmetic expression. This happens before any calculation takes place.

Watch what happens when you try to add two bytes together:

java
byte a = 127;
byte b = 1;
byte sum = a + b;         // COMPILE ERROR: incompatible types: possible lossy conversion from int to byte

You might expect the result to be another byte. It is not. The moment Java sees the expression a + b, it promotes both a and b to int. The expression a + b produces an int, not a byte. Storing an int result into a byte variable is a narrowing conversion, so the compiler rejects it.

The correct approaches are:

java
byte a = 127;
byte b = 1;

// Option 1: store the result as an int
int sum = a + b;            // 128 — no overflow because it was promoted to int first

// Option 2: explicitly cast back to byte
byte w = (byte)(a + b);    // -128 — the int result 128 is cast to byte, which overflows

Notice that in option 2, (byte)(a + b) correctly parenthesizes the entire expression first. If you wrote (byte)a + b, only a would be cast to byte, then immediately promoted back to int for the addition. The parentheses matter.

This promotion rule also applies to mixed type expressions. If an expression contains operands of different types, the entire expression is promoted to the widest type present:

java
int i = 34;
double d = 20.0;
double result = i + d;     // i is promoted to double, result is 54.0

// You cannot store this in an int without an explicit cast
int bad = i + d;           // COMPILE ERROR: possible lossy conversion from double to int
int ok  = (int)(i + d);   // 54 — explicit cast required

The promotion order: if any operand is double, the whole expression is double. If any operand is float, the whole expression is float. If any operand is long, the whole expression is long. Otherwise the expression is int.


The Five Kinds of Variables

This is another topic interviewers frequently explore. Java has one syntax for declaring a variable, but there are five completely different kinds based on where the declaration appears. Each kind has a different lifetime, different default behavior, and different access rules.

Here is all five in a single class:

java
public class Demo {

    // 1. Member variable (instance variable)
    int memberVar = 2;

    // 2. Static variable (class variable)
    static int staticVar = 1;

    // 3. Constructor parameter
    Demo(int constructorParam) {
        // 4. Local variable
        int localVar = 4;
        System.out.println(constructorParam + localVar);
    }

    // 5. Method parameter
    void calculate(int methodParam) {
        int localVar = 10;   // also a local variable
        System.out.println(methodParam + localVar);
    }
}

Member Variables (Instance Variables)

A member variable is declared inside the class body but outside any method. Every object you create from the class gets its own separate copy of that variable.

java
public class Employee {
    int salary = 50000;   // each Employee object has its own copy
}

If you create two Employee objects, changing salary on one does not affect the other. They are completely independent copies.

Member variables are automatically initialized to their default values when the object is created: 0 for numeric types, false for boolean, and null for reference types. You do not have to assign them a value yourself.

Static Variables (Class Variables)

A static variable is declared with the static keyword. Unlike member variables, there is only ever one copy of a static variable, shared among all objects of that class. It belongs to the class, not to any individual object.

java
public class Employee {
    static int companyCode = 100;   // one copy, shared by all Employee objects
}

The correct way to access a static variable is through the class name, not through an object reference:

java
Employee e1 = new Employee();
Employee e2 = new Employee();

Employee.companyCode = 999;            // correct: accessed via class name
System.out.println(e1.companyCode);    // works but poor style
System.out.println(e2.companyCode);    // also 999 — same variable

Because there is only one copy, any change made through one object or reference is visible to all others. That is why interviewers ask about static variables: it is a common source of bugs when developers expect each object to have its own copy but accidentally use static.

Local Variables

A local variable is declared inside a method. It exists only while that method is executing. When the method returns, the local variable is gone.

The critical difference from member and static variables: local variables are NOT automatically initialized. The compiler tracks every possible execution path and refuses to compile code that might read a local variable before it has been assigned a value:

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

You must always assign a value before using a local variable:

java
void check() {
    int x = 0;               // explicit initialization required
    System.out.println(x);   // now fine
}

Compare that with a member variable, which is automatically initialized:

java
public class Demo {
    int x;                        // member variable: auto-initialized to 0

    void print() {
        System.out.println(x);   // fine: x is 0 by default
    }
}

Method Parameters and Constructor Parameters

Method parameters and constructor parameters are the variables listed in parentheses in a method or constructor signature. Their values come from whoever calls the method or constructor.

java
void calculate(int a, int b) {   // a and b are method parameters
    System.out.println(a + b);
}

Demo(int value) {                 // value is a constructor parameter
    this.memberVar = value;
}

Method parameters and constructor parameters behave like local variables in terms of scope: they exist only for the duration of the method or constructor call. They do not have default values because they receive their values from the caller.


Putting It All Together: A Static Variable Demo

Here is the difference between member and static variables made concrete:

java
public class Employee {
    int memberSalary = 50000;       // each object gets its own copy
    static int companyCode = 101;   // one copy, shared by all objects

    public static void main(String[] args) {
        Employee e1 = new Employee();
        Employee e2 = new Employee();

        e1.memberSalary = 60000;
        System.out.println(e2.memberSalary);   // prints 50000: e2's copy unchanged

        Employee.companyCode = 999;
        System.out.println(e2.companyCode);    // prints 999: shared variable changed for everyone
        System.out.println(e1.companyCode);    // prints 999: same variable
    }
}

Interview Questions to Know Cold

Q: What is the difference between a statically typed and a strongly typed language?

Statically typed means every variable must declare its type at compile time, and the compiler checks types before the program runs. Strongly typed means there are hard limits on each type and you cannot silently cross type boundaries. Java is both.

Q: What is the range of byte, and why?

Byte is 8 bits and its range is 128 to 127. It uses two's complement signed representation. 8 bits gives 256 combinations. Zero through 127 use the 128 slots where the MSB is 0. 1 through 128 use the 128 slots where the MSB is 1. There is one more negative value than positive because zero takes a slot on the positive side.

Q: What is two's complement and why does Java use it?

Two's complement is how signed integers represent negative numbers. To negate a number: flip all bits, then add 1. Java uses it because the same binary addition circuit works for both positive and negative numbers. There is no separate subtraction circuit needed, and zero has a unique representation (no negative zero problem).

Q: What happens when you add 1 to a byte holding 127?

It overflows silently and becomes 128. In binary, 0111 1111 plus 0000 0001 equals 1000 0000, which in two's complement is 128. Java does not throw an exception.

Q: Why does this code not compile?

java
byte a = 127;
byte b = 1;
byte sum = a + b;

Because byte and short values are promoted to int in any arithmetic expression. The expression a + b produces an int, and storing an int into a byte requires an explicit cast. Either declare sum as int, or write byte sum = (byte)(a + b).

Q: What is the difference between member and static variables?

Every object gets its own copy of a member variable. There is only one copy of a static variable shared across all objects. Static variables belong to the class itself, accessed via the class name.

Q: Why must local variables be initialized before use, while member variables do not?

Member variables are initialized to their type defaults (0, false, null) when the object is created. Local variables have no default. The Java compiler performs flow analysis and refuses to compile code where a local variable might be read before it is written.

Q: Why should you never use float or double for currency?

They use binary floating point representation, which cannot exactly represent most decimal fractions. 0.1 + 0.2 evaluates to something like 0.30000000000000004. For exact decimal arithmetic, use BigDecimal with String constructor arguments.

Q: What is the difference between widening and narrowing conversion?

Widening moves from a smaller type to a larger type (byte to int, int to double). No data is lost and Java does it automatically. Narrowing moves from a larger type to a smaller type (double to int, long to byte). Data may be lost and Java requires an explicit cast.

Q: Why does float require an F suffix but double does not?

Because decimal literals like 63.2 are double by default. Assigning a double to a float is a narrowing conversion. The f suffix changes the literal from double to float, making the assignment a same type assignment with no narrowing.