Lesson 3 of 25

Variables & Data Types

Declaring a Variable, and Why the Type Comes First

A variable is a named box in memory that holds a value. In Java you must say what kind of value the box will hold before you put anything in it, and that type can never change afterwards. int marks = 87; reads as "make a box called marks that holds whole numbers, and put 87 in it".

If you have seen Python or JavaScript this feels like extra work, because there you simply write marks = 87. The payoff comes when you make a mistake. Assign a piece of text to that box later and Java refuses to compile, telling you exactly which line is wrong. In a dynamically typed language the same mistake compiles fine and fails at 2 a.m. when a user hits that code path.

One rule catches beginners immediately: a local variable — one declared inside a method — has no default value and must be given one before you read it. Java will not silently assume zero. Fields declared inside a class do get defaults (0 for numbers, false for booleans, null for objects), which is an inconsistency worth remembering because interviewers like it.

Example
int marks = 87;              // declare and assign together

double average;              // declare now...
average = 76.5;              // ...assign later

marks = 90;                  // fine — the value changes
// marks = "ninety";         // compile error: incompatible types

final int MAX_MARKS = 100;   // final = assign once, never again
// MAX_MARKS = 120;          // compile error: cannot assign a value to final variable

int total;
// System.out.println(total);
// compile error: variable total might not have been initialized
Notes
  • Use final more than you think you need to. It costs one word and it tells every future reader — including you next month — that this value is fixed. It also makes the compiler catch accidental reassignment for you.

The Eight Primitive Types

Java has exactly eight primitive types. They are not objects; they hold a raw value directly and have a fixed size in memory. Four hold whole numbers, two hold decimals, one holds a single character and one holds true or false.

In practice you will use int, double and boolean for almost everything, with long when numbers get large and char when working through text. byte and short exist for memory-constrained situations and for reading binary data; using them to "save space" in an ordinary program is a false economy, because the JVM often stores them in a full word anyway.

Two suffixes are compulsory and are the source of a puzzling early error. A whole-number literal is an int by default, so long population = 9000000000; fails to compile with integer number too large — the number does not fit in an int, and the fact that you are storing it in a long does not help, because the literal itself is evaluated first. Adding L fixes it. Similarly a decimal literal is a double by default, so float price = 9.99; fails; write 9.99f.

You can also put underscores inside numeric literals to make them readable — 1_00_00_000 groups a crore the way an Indian reader expects. The underscores are ignored by the compiler and exist purely for humans.

Example
// ---- Whole numbers ----
byte  small  = 127;                  //  8-bit, -128 to 127
short medium = 32_000;               // 16-bit, about -32k to +32k
int   count  = 2_000_000;            // 32-bit, about -2.1 to +2.1 billion
long  people = 9_000_000_000L;       // 64-bit  <- the L is required

// ---- Decimals ----
float  price = 9.99f;                // 32-bit  <- the f is required
double pi    = 3.141592653589793;    // 64-bit, the default choice

// ---- The other two ----
boolean isEnrolled = true;           // only true or false, never 0 or 1
char    grade      = 'A';            // ONE character, in SINGLE quotes

// Ranges are available as constants
System.out.println(Integer.MAX_VALUE);   // 2147483647
System.out.println(Integer.MIN_VALUE);   // -2147483648
Notes
  • 'A' with single quotes is a char. "A" with double quotes is a String. They are different types and cannot be swapped — char c = "A"; is a compile error. This trips up students coming from Python, where quotes are interchangeable.

Integer Overflow and Decimal Precision

Two numeric behaviours cause real bugs in student projects, and both look like the computer is simply wrong.

The first is overflow. An int holds values up to 2,147,483,647. Add one to that and Java does not raise an error — it silently wraps around to the most negative value. This matters more often than you expect: a rupee amount stored in paise, a count of milliseconds, or a population figure can all cross two billion. The moment your numbers might exceed roughly two billion, use long. The same wrap-around happens when you multiply two large ints, even if you assign the result to a long, because the multiplication itself is done in int arithmetic first.

The second is floating-point precision. double stores numbers in binary, and just as one-third cannot be written exactly in decimal, one-tenth cannot be written exactly in binary. So 0.1 + 0.2 comes out as 0.30000000000000004, and comparing it to 0.3 with == is false. This is not a Java bug; every language using the same hardware format behaves identically.

The practical rule is short: never store money in double or float. Either store the amount as a whole number of paise in a long, or use BigDecimal, which does exact decimal arithmetic. A billing system that loses a paisa per transaction is a bug you will be asked to explain.

Example
// ---- Overflow: no error, just a wrong answer ----
int big = Integer.MAX_VALUE;       // 2147483647
System.out.println(big + 1);       // -2147483648   <- wrapped around

// The subtle version — the multiplication is done as int, THEN widened
int a = 100_000, b = 100_000;
long wrong = a * b;                // 1410065408  (overflowed before assignment)
long right = (long) a * b;         // 10000000000 (cast one side first)

// ---- Decimal precision ----
System.out.println(0.1 + 0.2);         // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);  // false

// Money: use paise in a long, or BigDecimal
import java.math.BigDecimal;
BigDecimal p1 = new BigDecimal("0.1");
BigDecimal p2 = new BigDecimal("0.2");
System.out.println(p1.add(p2));        // 0.3  exactly
Notes
  • Build BigDecimal from a String, not from a double. new BigDecimal(0.1) copies the double's existing inaccuracy into the BigDecimal and prints a long trail of digits; new BigDecimal("0.1") is exact.

Casting: Widening Is Free, Narrowing Costs You

Converting a value from one numeric type to another is called casting, and Java splits it into two cases based on whether information can be lost.

Widening moves a value into a bigger type — int into long, int into double. Nothing can be lost, so Java does it automatically and you write nothing. Narrowing moves a value into a smaller type, where it may not fit, so Java forces you to write the cast explicitly. The explicit cast is you signing off on the risk.

Narrowing a decimal to an integer truncates — it chops the fractional part off rather than rounding. (int) 3.99 is 3, not 4. If you want rounding, use Math.round(). Narrowing an out-of-range integer wraps the bits around and gives a value with no obvious relationship to the original: (byte) 200 is -56.

The most consequential related rule is integer division. When both operands are whole numbers, Java performs whole-number division and discards the remainder, no matter what type you assign the result to. So 7 / 2 is 3, and double avg = 7 / 2; prints 3.0 — the division happened in int arithmetic before the widening. This single rule is responsible for more wrong average-marks calculations than anything else in a first Java course. Make one side a decimal and the whole expression becomes decimal arithmetic.

Example
// Widening — automatic, nothing is lost
int    x = 10;
long   y = x;          // 10
double z = x;          // 10.0

// Narrowing — you must write the cast, and you accept the loss
double d = 3.99;
int    i = (int) d;    // 3      <- truncated, NOT rounded
int    r = (int) Math.round(d);  // 4  <- if you wanted rounding

int  n = 200;
byte b = (byte) n;     // -56    <- wrapped, no warning

// ---- Integer division: the classic trap ----
int totalMarks = 7, subjects = 2;

double wrong = totalMarks / subjects;          // 3.0   <- int division first
double right = (double) totalMarks / subjects; // 3.5   <- cast one side
double also  = totalMarks / (double) subjects; // 3.5

System.out.println(7 % 2);    // 1  — the remainder that division threw away
Notes
  • char is a number underneath — it stores a 16-bit code. That is why (int) 'A' gives 65 and (char) 66 gives 'B', and why 'a' + 1 is the int 98 rather than the char 'b' until you cast it back.

Reference Types, Wrapper Classes and Autoboxing

Everything that is not one of the eight primitives is a reference type: String, arrays, and every class you or anyone else writes. The difference is what the variable holds. A primitive variable holds the value itself. A reference variable holds an address — a pointer to an object living elsewhere in memory — and can also hold null, meaning "pointing at nothing".

Each primitive has a matching wrapper class — int has Integer, double has Double, char has Character, boolean has Boolean, and so on. Wrappers exist because collections such as ArrayList can only hold objects, never primitives. You cannot write List<int>; you write List<Integer>.

Since Java 5 the conversion between the two happens automatically, which is called autoboxing in one direction and unboxing in the other. It is convenient and it hides two real hazards. The first is performance: boxing in a tight loop creates millions of short-lived objects, so prefer int over Integer for loop counters and arithmetic.

The second hazard is far nastier. Unboxing a null wrapper throws a NullPointerException on a line that contains no visible method call at all. A Map<String, Integer> returns null for a missing key; assigning that result to an int unboxes it and explodes. The line looks like harmless arithmetic, which is why it is so hard to spot. Use getOrDefault, or keep the value as an Integer and check it for null before using it.

Example
import java.util.*;

// Wrappers are needed for collections
List<Integer> marks = new ArrayList<>();
marks.add(87);          // autoboxing: int 87 -> Integer.valueOf(87)
int first = marks.get(0);   // unboxing: Integer -> int

// ---- The NullPointerException that has no method call on the line ----
Map<String, Integer> scores = new HashMap<>();
scores.put("maths", 92);

// int physics = scores.get("physics");
// NullPointerException — get() returned null, and unboxing null blows up

int physics = scores.getOrDefault("physics", 0);   // 0 — safe

Integer maybe = scores.get("physics");             // null, no exception yet
if (maybe != null) {
    System.out.println(maybe + 10);
}

// Useful wrapper methods you will use constantly
int parsed = Integer.parseInt("42");        // String -> int
double dp  = Double.parseDouble("3.14");
String text = String.valueOf(42);           // int -> String
Notes
  • Integer.parseInt("42abc") throws NumberFormatException, not zero and not null. Any time you convert user input to a number, assume it may not be a number.
  • A famous puzzle: two Integer variables both set to 127 compare == as true, but both set to 128 compare as false. Java caches boxed Integers from -128 to 127 and reuses the same object, so == accidentally works in the small range. Never use == on wrappers — use .equals(), or unbox to int first.

var, and Naming Things Properly

Java 10 added var, which lets the compiler work out the type from the value on the right-hand side. This is not the dynamic typing of JavaScript. The variable still has one fixed type forever; you simply did not write it down. var count = 42; creates an int, and assigning a String to it later is still a compile error.

var only works on local variables inside methods. It cannot be used for fields, method parameters or return types, and it needs an initialiser on the same line, so var x; does not compile. Use it where it removes genuine noise — the right-hand side already names the type, as in var scanner = new Scanner(System.in); — and avoid it where it hides information, as in var result = process(data);, where a reader now has no idea what they are holding.

Naming is not decoration. Java has strong conventions that every codebase and every interviewer expects, and following them is free.

Example
// var infers the type — still statically typed
var count = 42;                          // int
var name = "Ananya";                     // String
var list = new ArrayList<String>();      // ArrayList<String>

// count = "forty two";   // still a compile error

// var will not compile here:
// var x;                 // no initialiser
// var y = null;          // nothing to infer from

// Good use — the type is already obvious on the right
var scanner = new java.util.Scanner(System.in);

// Poor use — the reader now has to go and find out what process() returns
// var result = process(data);
  • Variables and methods: camelCase starting lower case — totalMarks, calculateAverage()
  • Classes and interfaces: PascalCase starting upper case — BankAccount, StudentRecord
  • Constants (static final): UPPER_SNAKE_CASE — MAX_MARKS, PI
  • Packages: all lower case, reverse domain style — com.example.billing
  • Booleans read as questions — isActive, hasPaid, canEdit — so the if statement reads like English
  • Names may contain letters, digits, _ and $, but cannot start with a digit and cannot be a keyword such as class or int
Ask AI