In the previous tutorial we discussed Java Variables and learned that every variable must have three things: a type, a name, and a value. The type (also called data type) tells Java what kind of data the variable can hold and how much memory to allocate for it. In this tutorial, we will take a deep dive into Java data types.
Quick recap on variables:
public static void main(String[] args) {
// type name value
String firstName = "Folau";
int age = 30;
double salary = 85000.50;
System.out.println(firstName); // Folau
System.out.println(age); // 30
System.out.println(salary); // 85000.5
}
Java data types fall into two broad categories:
byte, short, int, long, float, double, boolean, and char. They hold their values directly in memory and are not objects.String, Arrays, List, and any class you define.Understanding the difference is fundamental to writing correct, efficient Java code. Let us start with the primitives.
Java has exactly 8 primitive data types. Each has a fixed size in memory and a defined range of values it can hold. Here is a summary:
| Type | Size | Range | Default Value | When to Use |
|---|---|---|---|---|
byte |
1 byte (8 bits) | -128 to 127 | 0 | Raw binary data, file I/O, saving memory in large arrays |
short |
2 bytes (16 bits) | -32,768 to 32,767 | 0 | Rarely used; memory-sensitive applications with small numbers |
int |
4 bytes (32 bits) | -2,147,483,648 to 2,147,483,647 | 0 | Default choice for whole numbers, loop counters, general arithmetic |
long |
8 bytes (64 bits) | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | 0L | Timestamps, database IDs, values exceeding int range |
float |
4 bytes (32 bits) | ±3.4 × 1038 (6-7 decimal digits of precision) | 0.0f | Graphics, game engines, when memory matters more than precision |
double |
8 bytes (64 bits) | ±1.7 × 10308 (15 decimal digits of precision) | 0.0d | Default for decimal numbers, scientific calculations, currency (with caution) |
boolean |
1 bit* | true or false |
false | Flags, conditions, on/off states |
char |
2 bytes (16 bits) | 0 to 65,535 (Unicode characters) | ‘\u0000’ | Single characters, Unicode processing |
* The JVM does not define a precise size for boolean. It typically uses 1 byte internally, but the logical size is a single bit.
These four types store whole numbers (no decimal point). The only difference between them is how much memory they use and the range of values they can hold.
The byte type is the smallest integer type. It is commonly used when reading raw binary data from files or network streams, or when you need to save memory in large arrays.
public static void main(String[] args) {
byte minByte = -128;
byte maxByte = 127;
byte userAge = 25; // Age fits easily in a byte
System.out.println("Min byte: " + minByte); // -128
System.out.println("Max byte: " + maxByte); // 127
System.out.println("Age: " + userAge); // 25
// Useful for binary data
byte[] fileData = {72, 101, 108, 108, 111}; // "Hello" in ASCII
System.out.println(new String(fileData)); // Hello
}
The short type is rarely used in modern Java. It can hold values from -32,768 to 32,767. You might encounter it in legacy code or specialized applications where memory savings on large datasets matter.
public static void main(String[] args) {
short temperature = -15;
short elevation = 8849; // Mount Everest height in meters
short year = 2026;
System.out.println("Temperature: " + temperature + "°C"); // -15°C
System.out.println("Elevation: " + elevation + "m"); // 8849m
System.out.println("Year: " + year); // 2026
}
The int type is the default choice for whole numbers in Java. Unless you have a specific reason to use another type, use int. It can hold values up to approximately 2.1 billion, which is sufficient for most use cases like loop counters, array indices, quantities, and general arithmetic.
public static void main(String[] args) {
int population = 331000000; // US population
int salary = 95000;
int itemCount = 0;
// Common use: loop counter
for (int i = 0; i < 5; i++) {
itemCount++;
}
System.out.println("Population: " + population); // 331000000
System.out.println("Salary: $" + salary); // $95000
System.out.println("Items: " + itemCount); // 5
// You can use underscores for readability (Java 7+)
int billion = 1_000_000_000;
System.out.println("Billion: " + billion); // 1000000000
}
The long type is used when int is not large enough. Common scenarios include timestamps (milliseconds since epoch), database primary key IDs, file sizes in bytes, and large financial calculations. Long literals must end with an L suffix.
public static void main(String[] args) {
long worldPopulation = 8_000_000_000L; // 8 billion - too large for int!
long timestamp = System.currentTimeMillis();
long fileSize = 5_368_709_120L; // 5 GB in bytes
long databaseId = 9_876_543_210L;
System.out.println("World population: " + worldPopulation);
System.out.println("Current timestamp: " + timestamp);
System.out.println("File size: " + fileSize + " bytes");
System.out.println("DB ID: " + databaseId);
// Without the L suffix, Java treats the number as an int and you get an error
// long wrong = 9876543210; // COMPILE ERROR: integer number too large
long correct = 9876543210L; // Works because of the L suffix
}
These types store decimal (fractional) numbers. The key difference between them is precision — how many decimal digits they can accurately represent.
The float type provides about 6-7 digits of precision. Float literals must end with an f suffix. It is used in graphics programming, game engines, and situations where memory is more important than precision.
public static void main(String[] args) {
float pi = 3.14159f; // Note the 'f' suffix
float temperature = 98.6f;
float gpa = 3.85f;
float price = 19.99f;
System.out.println("Pi: " + pi); // 3.14159
System.out.println("Temp: " + temperature); // 98.6
System.out.println("GPA: " + gpa); // 3.85
System.out.println("Price: $" + price); // $19.99
// Without 'f', Java treats decimal numbers as double and you get an error
// float wrong = 3.14; // COMPILE ERROR: incompatible types
float correct = 3.14f; // Works because of the f suffix
}
The double type is the default choice for decimal numbers in Java. It provides about 15 digits of precision, which is more than enough for most applications. When you write a decimal literal like 3.14, Java automatically treats it as a double.
public static void main(String[] args) {
double pi = 3.141592653589793;
double accountBalance = 1_250_000.75;
double interestRate = 0.045; // 4.5%
System.out.println("Pi: " + pi); // 3.141592653589793
System.out.println("Balance: $" + accountBalance); // $1250000.75
// Calculate interest
double interest = accountBalance * interestRate;
System.out.println("Interest: $" + interest); // $56250.03375
// Precision difference between float and double
float floatVal = 1.123456789f;
double doubleVal = 1.123456789;
System.out.println("float: " + floatVal); // 1.1234568 (lost precision after 7 digits)
System.out.println("double: " + doubleVal); // 1.123456789 (accurate to 15 digits)
}
The boolean type can hold only two values: true or false. It is used for flags, conditions, and logical expressions. Booleans are the backbone of control flow in Java — every if statement, while loop, and conditional expression evaluates to a boolean.
public static void main(String[] args) {
boolean isLoggedIn = true;
boolean isAdmin = false;
boolean hasPermission = true;
// Used in conditions
if (isLoggedIn && hasPermission) {
System.out.println("Access granted"); // This prints
}
if (isAdmin) {
System.out.println("Welcome, admin");
} else {
System.out.println("Welcome, user"); // This prints
}
// Boolean from comparisons
int age = 21;
boolean canVote = age >= 18;
boolean isTeenager = age >= 13 && age <= 19;
System.out.println("Can vote: " + canVote); // true
System.out.println("Is teenager: " + isTeenager); // false
// Boolean with methods
String email = "user@example.com";
boolean validEmail = email.contains("@");
System.out.println("Valid email: " + validEmail); // true
}
The char type stores a single character. In Java, characters use Unicode (UTF-16) encoding, which means char can represent not just English letters but characters from virtually any language, as well as symbols and emojis. A char value is enclosed in single quotes ('A'), unlike strings which use double quotes ("A").
public static void main(String[] args) {
char letter = 'A';
char digit = '7';
char symbol = '$';
char space = ' ';
System.out.println("Letter: " + letter); // A
System.out.println("Digit: " + digit); // 7
System.out.println("Symbol: " + symbol); // $
// char is actually a number (Unicode code point)
char letterA = 65; // 65 is the Unicode value for 'A'
System.out.println(letterA); // A
// Unicode escape sequences
char copyright = '\u00A9'; // copyright symbol
char omega = '\u03A9'; // Greek capital letter Omega
System.out.println("Copyright: " + copyright); // ©
System.out.println("Omega: " + omega); // Ω
// You can do arithmetic with char (it is a numeric type)
char ch = 'A';
ch++;
System.out.println(ch); // B
// Convert char to its numeric value
char c = 'Z';
int asciiValue = (int) c;
System.out.println("ASCII value of 'Z': " + asciiValue); // 90
}
Non-primitive data types are also called reference types because a variable of this type does not hold the value directly — it holds a reference (a pointer) to an object in memory. Reference types are created from classes and include String, arrays, collections, and any custom class you define.
String is the most commonly used reference type. Although it is not a primitive, it is so fundamental that Java gives it special treatment (you can create a String without using the new keyword). We cover Strings in detail in the Java String tutorial.
public static void main(String[] args) {
// String is a reference type, but you can create it like a primitive
String name = "Folau";
String greeting = "Hello, " + name + "!";
System.out.println(greeting); // Hello, Folau!
System.out.println(name.length()); // 5
System.out.println(name.toUpperCase()); // FOLAU
// Strings are objects with methods - primitives do not have methods
boolean startsWithF = name.startsWith("F");
System.out.println(startsWithF); // true
}
Arrays are reference types that hold a fixed-size collection of elements of the same type. We cover arrays in detail in the Java Arrays tutorial.
public static void main(String[] args) {
// Array of integers
int[] scores = {95, 87, 73, 100, 88};
// Array of strings
String[] names = {"Alice", "Bob", "Charlie"};
System.out.println("First score: " + scores[0]); // 95
System.out.println("Second name: " + names[1]); // Bob
System.out.println("Array length: " + scores.length); // 5
}
Any class you define creates a new reference type. When you create an object using the new keyword, you get a reference to that object in memory.
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
void greet() {
System.out.println("Hi, I'm " + name + " and I'm " + age + " years old.");
}
}
public static void main(String[] args) {
// 'user' is a reference variable - it holds the memory address of a User object
User user = new User("Folau", 30);
user.greet(); // Hi, I'm Folau and I'm 30 years old.
// Reference types can be null - primitives cannot
User emptyUser = null;
System.out.println(emptyUser); // null
}
| Feature | Primitive Types | Non-Primitive (Reference) Types |
|---|---|---|
| Defined by | Built into the Java language | Created by the programmer (except String) |
| Naming | Start with lowercase: int, double, boolean |
Start with uppercase: String, User, List |
| Stores | The actual value directly | A reference (memory address) to the object |
| Can be null? | No — always has a value | Yes — can be null |
| Has methods? | No | Yes — e.g., name.length(), user.greet() |
| Memory | Stored on the stack (fast access) | Object stored on the heap, reference on the stack |
| Default value | 0, 0.0, false, or '\u0000' |
null |
Type casting is the process of converting a value from one data type to another. In Java, there are two types of casting:
Widening casting happens automatically when you assign a smaller type to a larger type. There is no risk of data loss because the larger type can hold all possible values of the smaller type.
The widening order is:
byte → short → int → long → float → double
public static void main(String[] args) {
// Widening: Java automatically converts to the larger type
byte myByte = 42;
short myShort = myByte; // byte -> short (automatic)
int myInt = myShort; // short -> int (automatic)
long myLong = myInt; // int -> long (automatic)
float myFloat = myLong; // long -> float (automatic)
double myDouble = myFloat; // float -> double (automatic)
System.out.println("byte: " + myByte); // 42
System.out.println("short: " + myShort); // 42
System.out.println("int: " + myInt); // 42
System.out.println("long: " + myLong); // 42
System.out.println("float: " + myFloat); // 42.0
System.out.println("double: " + myDouble); // 42.0
// This is also widening (int to double)
int score = 85;
double percentage = score; // Automatically converts to 85.0
System.out.println("Percentage: " + percentage); // 85.0
}
Narrowing casting must be done manually by placing the target type in parentheses before the value. This is required when converting a larger type to a smaller type because there is a risk of data loss — the value might not fit in the smaller type.
The narrowing order is:
double → float → long → int → short → byte
public static void main(String[] args) {
// Narrowing: you must explicitly cast with (type)
double myDouble = 9.78;
int myInt = (int) myDouble; // Truncates the decimal, does NOT round
System.out.println("double: " + myDouble); // 9.78
System.out.println("int: " + myInt); // 9 (decimal part lost!)
// More examples
int largeNumber = 130;
byte smallByte = (byte) largeNumber;
System.out.println("int: " + largeNumber); // 130
System.out.println("byte: " + smallByte); // -126 (overflow! 130 exceeds byte range)
double price = 49.99;
float floatPrice = (float) price; // double -> float
long longPrice = (long) price; // double -> long (truncates decimal)
int intPrice = (int) price; // double -> int (truncates decimal)
System.out.println("double: " + price); // 49.99
System.out.println("float: " + floatPrice); // 49.99
System.out.println("long: " + longPrice); // 49
System.out.println("int: " + intPrice); // 49
// Without the cast, you get a compile error
// int x = 3.14; // COMPILE ERROR
int x = (int) 3.14; // OK - x is 3
}
Every primitive type has a corresponding wrapper class that wraps the primitive value in an object. Wrapper classes are part of the java.lang package and are essential when you need to use primitives as objects — for example, in collections like ArrayList, which cannot hold primitive types directly.
| Primitive Type | Wrapper Class |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
boolean |
Boolean |
char |
Character |
Wrapper classes also provide useful utility methods for parsing, converting, and working with their respective types.
public static void main(String[] args) {
// Wrapper classes provide useful utility methods
// Parsing strings to numbers
int num = Integer.parseInt("42");
double price = Double.parseDouble("19.99");
boolean flag = Boolean.parseBoolean("true");
System.out.println(num); // 42
System.out.println(price); // 19.99
System.out.println(flag); // true
// Getting min and max values
System.out.println("int max: " + Integer.MAX_VALUE); // 2147483647
System.out.println("int min: " + Integer.MIN_VALUE); // -2147483648
System.out.println("double max: " + Double.MAX_VALUE); // 1.7976931348623157E308
// Converting numbers to strings
String numStr = Integer.toString(42);
String hexStr = Integer.toHexString(255); // "ff"
String binStr = Integer.toBinaryString(10); // "1010"
System.out.println("Hex: " + hexStr); // ff
System.out.println("Binary: " + binStr); // 1010
// Comparing values
int result = Integer.compare(10, 20);
System.out.println("Compare 10 vs 20: " + result); // -1 (10 < 20)
}
Since Java 5, the compiler can automatically convert between primitives and their wrapper classes. This feature eliminates the need for manual conversion in most cases.
int → Integer)Integer → int)import java.util.ArrayList;
import java.util.List;
public static void main(String[] args) {
// AUTOBOXING: primitive -> wrapper (automatic)
Integer wrappedInt = 42; // int 42 is autoboxed to Integer
Double wrappedDouble = 3.14; // double 3.14 is autoboxed to Double
Boolean wrappedBool = true; // boolean true is autoboxed to Boolean
System.out.println(wrappedInt); // 42
System.out.println(wrappedDouble); // 3.14
System.out.println(wrappedBool); // true
// UNBOXING: wrapper -> primitive (automatic)
int primitiveInt = wrappedInt; // Integer is unboxed to int
double primitiveDouble = wrappedDouble; // Double is unboxed to double
System.out.println(primitiveInt); // 42
System.out.println(primitiveDouble); // 3.14
// Autoboxing is essential for Collections
// ArrayList cannot hold primitive 'int', but autoboxing handles it
List numbers = new ArrayList<>();
numbers.add(10); // autoboxing: int -> Integer
numbers.add(20); // autoboxing: int -> Integer
numbers.add(30); // autoboxing: int -> Integer
// Unboxing when retrieving
int first = numbers.get(0); // unboxing: Integer -> int
System.out.println("First: " + first); // 10
// Autoboxing in arithmetic expressions
Integer a = 10;
Integer b = 20;
int sum = a + b; // Both are unboxed, added, result stays as int
System.out.println("Sum: " + sum); // 30
}
Choosing the right data type is an important decision. Here is practical guidance for real-world scenarios:
| Scenario | Recommended Type | Reason |
|---|---|---|
| Loop counters, array indices | int |
Default choice for whole numbers; fast and sufficient range |
| Age, quantity, count | int |
Simple whole numbers within normal ranges |
| Database IDs | long |
IDs can exceed int range in large databases |
| Timestamps | long |
Milliseconds since epoch requires long range |
| File sizes | long |
Files can exceed 2 GB (int max) |
| Prices, financial calculations | BigDecimal |
Avoid float/double for money — use java.math.BigDecimal for exact precision |
| Scientific calculations | double |
15 digits of precision; default for decimals |
| Graphics, game coordinates | float |
Sufficient precision, half the memory of double |
| Yes/no flags, feature toggles | boolean |
Only two possible values |
| Single characters, Unicode | char |
One character at a time |
| Raw binary data, byte streams | byte |
File I/O and network data operate on bytes |
| Text, names, messages | String |
Reference type for sequences of characters |
| Collections (ArrayList, HashMap) | Wrapper classes | Collections require objects, not primitives |
Rule of thumb: Use int for whole numbers and double for decimals unless you have a specific reason to choose another type. When in doubt, go bigger — the memory savings from using byte or short are rarely worth the complexity.
Even experienced developers make these mistakes. Understanding them early will save you hours of debugging.
When a value exceeds the maximum (or minimum) range of its type, it "wraps around" to the other end. Java does not throw an error — it silently overflows, which can produce very unexpected results.
public static void main(String[] args) {
// Integer overflow - the value wraps around silently!
int maxInt = Integer.MAX_VALUE; // 2,147,483,647
int overflow = maxInt + 1;
System.out.println("Max int: " + maxInt); // 2147483647
System.out.println("Max int + 1: " + overflow); // -2147483648 (wrapped to minimum!)
// This is a common bug when calculating large values
int million = 1_000_000;
int result = million * million; // 1,000,000,000,000 - too big for int!
System.out.println("Million squared (int): " + result); // -727379968 (wrong!)
// Fix: use long
long correctResult = (long) million * million;
System.out.println("Million squared (long): " + correctResult); // 1000000000000
}
Floating-point numbers (float and double) cannot represent all decimal values exactly. This is a fundamental limitation of how computers store decimal numbers in binary. Never use float or double for financial calculations.
import java.math.BigDecimal;
public static void main(String[] args) {
// Floating-point precision problem
double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println("0.1 + 0.2 = " + sum); // 0.30000000000000004 (not 0.3!)
System.out.println("0.1 + 0.2 == 0.3? " + (sum == 0.3)); // false!
// This is why you should NEVER use double for money
double price = 0.10;
double quantity = 3;
double total = price * quantity;
System.out.println("$0.10 x 3 = $" + total); // $0.30000000000000004
// Use BigDecimal for financial calculations
BigDecimal bdPrice = new BigDecimal("0.10");
BigDecimal bdQuantity = new BigDecimal("3");
BigDecimal bdTotal = bdPrice.multiply(bdQuantity);
System.out.println("BigDecimal: $0.10 x 3 = $" + bdTotal); // $0.30
}
Using == on wrapper objects compares references, not values. This can produce surprising results because Java caches Integer values between -128 and 127.
public static void main(String[] args) {
// Cached range (-128 to 127): == works because Java reuses the same objects
Integer x = 100;
Integer y = 100;
System.out.println(x == y); // true (cached, same object)
System.out.println(x.equals(y)); // true
// Outside cached range: == fails because they are different objects
Integer a = 200;
Integer b = 200;
System.out.println(a == b); // false! (different objects in memory)
System.out.println(a.equals(b)); // true (correct way to compare)
// ALWAYS use .equals() to compare wrapper objects
}
If a wrapper object is null and you try to unbox it to a primitive, Java throws a NullPointerException.
public static void main(String[] args) {
Integer wrappedValue = null;
// This throws NullPointerException at runtime!
// int primitiveValue = wrappedValue; // unboxing null -> crash
// Safe approach: check for null first
if (wrappedValue != null) {
int safeValue = wrappedValue;
System.out.println(safeValue);
} else {
System.out.println("Value is null, using default: 0");
int safeValue = 0;
}
}
Long literals need an L suffix, and float literals need an f suffix. Without them, Java treats the value as int or double, which can cause compile errors or unexpected behavior.
public static void main(String[] args) {
// WRONG: no suffix
// long bigNumber = 3000000000; // COMPILE ERROR: integer too large
// float pi = 3.14; // COMPILE ERROR: incompatible types
// CORRECT: with suffix
long bigNumber = 3000000000L; // L suffix for long
float pi = 3.14f; // f suffix for float
System.out.println(bigNumber); // 3000000000
System.out.println(pi); // 3.14
// Tip: Use uppercase L to avoid confusion with the digit 1
long value = 123456789L; // Clear
// long value = 123456789l; // Confusing - lowercase l looks like 1
}
byte, short, int, long, float, double, boolean, and char.String, arrays, and any class you define. They hold references to objects, not the values themselves.int as your default for whole numbers and double as your default for decimals.Integer, Double, etc.) for use in collections and APIs that require objects.null values causing NullPointerException.In the next tutorial, we will cover Java Operators, where you will learn how to perform arithmetic, comparisons, and logical operations on these data types.
Previous: Java Variables | Next: Java Operators
Think of a variable as a labeled box in your computer’s memory. You give the box a name, decide what kind of thing it can hold, and then put a value inside it. Later, you can look at the value, change it, or pass it along to another part of your program.
More formally, a variable is a named memory location that stores a value of a specific data type. Every Java program you write will be full of variables, so understanding them deeply is one of the most important steps in becoming a strong developer.
// A simple variable: a labeled box named "age" that holds the integer 30 int age = 30; System.out.println(age); // Output: 30
In Java, creating a variable is a two-part process: declaration and initialization. You can do them separately or on the same line.
Declaration tells the compiler: “Reserve a spot in memory with this name and this type.”
Initialization tells the compiler: “Put this value in that spot.”
The general syntax is:
type name = value;
// Declaration and initialization on the same line String firstName = "Folau"; int numberOfItems = 42; double accountBalance = 1250.75; boolean isActive = true; // Declaration first, initialization later String lastName; lastName = "Kaveinga"; // Declaring multiple variables of the same type on one line int x = 1, y = 2, z = 3; System.out.println(firstName); // Output: Folau System.out.println(lastName); // Output: Kaveinga System.out.println(numberOfItems); // Output: 42 System.out.println(accountBalance); // Output: 1250.75 System.out.println(isActive); // Output: true System.out.println(x + y + z); // Output: 6
A variable has three essential parts:
1. Type – What kind of data it holds (int, String, double, boolean, etc.)
2. Name – The label you use to refer to it in your code
3. Value – The actual data stored in memory
The equals sign (=) is the assignment operator. It takes the value on the right and stores it in the variable on the left. This is not the same as mathematical equality. It means “assign this value to this variable.”
// The assignment operator in action String firstName = "Folau"; System.out.println(firstName); // Output: Folau // Reassigning a new value to the same variable firstName = "Lisa"; System.out.println(firstName); // Output: Lisa // The old value "Folau" is gone. The box now holds "Lisa".
Java has three categories of variables based on where they are declared. Understanding the differences is critical because it affects their lifetime, default values, and visibility.
Local variables are declared inside a method, constructor, or block. They exist only while that method or block is executing, and they are destroyed once execution leaves that scope.
Key facts about local variables:
– They must be initialized before use. Java will not assign a default value. If you try to use an uninitialized local variable, the compiler will throw an error.
– They are not accessible outside the method or block where they are declared.
– They cannot have access modifiers (public, private, protected) because they belong to the method, not the class.
public class LocalVariableExample {
public void calculateTotal() {
// These are local variables - they exist only inside this method
int quantity = 5;
double pricePerItem = 19.99;
double total = quantity * pricePerItem;
System.out.println("Total: $" + total); // Output: Total: $99.95
}
public void anotherMethod() {
// This will NOT compile - "total" does not exist here
// System.out.println(total); // ERROR: cannot find symbol
// You must declare your own variables in this scope
double total = 0.0;
System.out.println("Total: $" + total); // Output: Total: $0.0
}
public static void main(String[] args) {
LocalVariableExample example = new LocalVariableExample();
example.calculateTotal();
example.anotherMethod();
}
}
Instance variables are declared inside a class but outside any method. Each object (instance) of the class gets its own copy of these variables. If you create three Person objects, each one has its own separate name and age.
Key facts about instance variables:
– They receive default values if you do not initialize them (0 for numbers, false for booleans, null for objects).
– They exist as long as the object exists.
– They can have access modifiers (private, public, protected).
public class Person {
// Instance variables - each Person object gets its own copy
private String name;
private int age;
private String email;
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public void introduce() {
System.out.println("Hi, I'm " + name + ", age " + age);
}
public static void main(String[] args) {
// Two separate objects, each with their own instance variables
Person person1 = new Person("Folau", 30, "folau@example.com");
Person person2 = new Person("Lisa", 28, "lisa@example.com");
person1.introduce(); // Output: Hi, I'm Folau, age 30
person2.introduce(); // Output: Hi, I'm Lisa, age 28
// Changing person1's name does NOT affect person2
// Each object owns its own copy of the instance variables
}
}
Static variables are declared with the static keyword inside a class but outside any method. Unlike instance variables, there is only one copy shared across all instances of the class. If one object changes a static variable, every other object sees the change.
Static variables are perfect for values that should be the same for every instance, such as a counter that tracks how many objects have been created, or a configuration value shared by the whole application.
public class Employee {
// Static variable - shared by ALL Employee objects
private static int employeeCount = 0;
private static String companyName = "LoveMeSomeCoding";
// Instance variables - each Employee gets its own copy
private String name;
private int id;
public Employee(String name) {
this.name = name;
employeeCount++; // Increment the shared counter
this.id = employeeCount; // Assign a unique ID
}
public void displayInfo() {
System.out.println("ID: " + id + ", Name: " + name + ", Company: " + companyName);
}
public static int getEmployeeCount() {
return employeeCount;
}
public static void main(String[] args) {
Employee emp1 = new Employee("Folau");
Employee emp2 = new Employee("Lisa");
Employee emp3 = new Employee("James");
emp1.displayInfo(); // Output: ID: 1, Name: Folau, Company: LoveMeSomeCoding
emp2.displayInfo(); // Output: ID: 2, Name: Lisa, Company: LoveMeSomeCoding
emp3.displayInfo(); // Output: ID: 3, Name: James, Company: LoveMeSomeCoding
// The static variable is shared - all three objects see the same count
System.out.println("Total employees: " + Employee.getEmployeeCount());
// Output: Total employees: 3
}
}
Scope defines where a variable can be accessed in your code. Once you leave a variable’s scope, it no longer exists. Java has three levels of scope you need to understand.
A variable declared inside a pair of curly braces { } only exists within those braces. This includes if-statements, for-loops, while-loops, and any other block.
A variable declared at the top of a method is accessible anywhere within that method, but not outside of it. Method parameters also have method scope.
Instance and static variables have class scope. They are accessible from any method within the class (and potentially from outside the class, depending on their access modifier).
public class ScopeDemo {
// Class scope - accessible from any method in this class
private String classLevelVar = "I'm available everywhere in this class";
public void demonstrateScope() {
// Method scope - accessible anywhere in this method
int methodLevelVar = 100;
System.out.println(classLevelVar); // OK - class scope
System.out.println(methodLevelVar); // OK - method scope
if (methodLevelVar > 50) {
// Block scope - only exists inside this if-block
String blockLevelVar = "I only exist inside this if-block";
System.out.println(blockLevelVar); // OK - we are inside the block
System.out.println(methodLevelVar); // OK - method scope is wider
}
// This will NOT compile - blockLevelVar is out of scope
// System.out.println(blockLevelVar); // ERROR: cannot find symbol
for (int i = 0; i < 3; i++) {
// "i" only exists inside this for-loop
System.out.println("Loop index: " + i);
}
// This will NOT compile - "i" is out of scope
// System.out.println(i); // ERROR: cannot find symbol
}
public void anotherMethod() {
System.out.println(classLevelVar); // OK - class scope
// System.out.println(methodLevelVar); // ERROR - method scope is limited to demonstrateScope()
}
public static void main(String[] args) {
ScopeDemo demo = new ScopeDemo();
demo.demonstrateScope();
}
}
Starting with Java 10, you can use var to let the compiler figure out the type for you. This is called local variable type inference. The compiler looks at the value on the right side of the assignment and determines the type automatically.
Important rules about var:
- var only works with local variables. You cannot use it for instance variables, static variables, or method parameters.
- The variable must be initialized on the same line. The compiler needs the right-hand side to infer the type.
- var is not a keyword technically - it is a reserved type name, so you could still name a variable "var" (though you should not).
- The type is still determined at compile time. Java is still statically typed. var is just syntactic convenience, not dynamic typing.
public class VarExample {
public static void main(String[] args) {
// Without var - explicit types
String name = "Folau";
int age = 30;
List hobbies = new ArrayList<>();
// With var - the compiler infers the types
var name2 = "Lisa"; // Inferred as String
var age2 = 28; // Inferred as int
var hobbies2 = new ArrayList(); // Inferred as ArrayList
System.out.println(name2.getClass().getSimpleName()); // Output: String
System.out.println(((Object) age2).getClass().getSimpleName()); // Output: Integer
// var is great for reducing verbosity with long type names
var employeeMap = new HashMap>();
// Without var, that same line would be:
// HashMap> employeeMap = new HashMap>();
// These will NOT compile:
// var x; // ERROR: cannot infer type - no initializer
// var y = null; // ERROR: cannot infer type - null has no type
}
}
A good rule of thumb: use var when the type is obvious from the right-hand side of the assignment. If it makes the code harder to read, stick with the explicit type.
If you have a value that should never change after it is set, mark it with the final keyword. Once assigned, a final variable cannot be reassigned. This is how you create constants in Java.
By convention, constants are named using UPPER_SNAKE_CASE (all uppercase letters with underscores between words). When combined with the static keyword, a final variable becomes a true class-level constant shared across all instances.
public class ConstantExample {
// Class-level constants: static + final
// Convention: UPPER_SNAKE_CASE
public static final double PI = 3.14159265358979;
public static final int MAX_LOGIN_ATTEMPTS = 5;
public static final String DEFAULT_CURRENCY = "USD";
// Instance-level final variable - set once per object, then locked
private final String createdAt;
public ConstantExample() {
this.createdAt = java.time.LocalDateTime.now().toString();
// createdAt cannot be changed after this point
}
public void tryToBreakConstants() {
// This will NOT compile - cannot reassign a final variable
// PI = 3.14; // ERROR: cannot assign a value to final variable PI
// Local final variable
final int maxRetries = 3;
// maxRetries = 5; // ERROR: cannot assign a value to final variable maxRetries
System.out.println("PI: " + PI);
System.out.println("Max login attempts: " + MAX_LOGIN_ATTEMPTS);
System.out.println("Created at: " + createdAt);
}
public static void main(String[] args) {
ConstantExample example = new ConstantExample();
example.tryToBreakConstants();
// Output:
// PI: 3.14159265358979
// Max login attempts: 5
// Created at: 2026-02-28T10:30:00.123456
}
}
When you declare an instance or static variable without assigning a value, Java automatically gives it a default value. This does not apply to local variables. Local variables have no default and must be initialized before use.
Here is the full table of default values:
| Data Type | Default Value |
|---|---|
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0 |
| char | '\u0000' (null character) |
| boolean | false |
| Any object (String, arrays, etc.) | null |
public class DefaultValueDemo {
// Instance variables with no explicit initialization
int number;
double decimal;
boolean flag;
char character;
String text;
int[] numbers;
public void printDefaults() {
System.out.println("int default: " + number); // Output: 0
System.out.println("double default: " + decimal); // Output: 0.0
System.out.println("boolean default: " + flag); // Output: false
System.out.println("char default: [" + character + "]"); // Output: [ ] (null character)
System.out.println("String default: " + text); // Output: null
System.out.println("Array default: " + numbers); // Output: null
}
public void localVariableExample() {
int localNumber;
// This will NOT compile - local variables have no default value
// System.out.println(localNumber); // ERROR: variable localNumber might not have been initialized
// You must assign a value first
localNumber = 10;
System.out.println(localNumber); // Output: 10
}
public static void main(String[] args) {
DefaultValueDemo demo = new DefaultValueDemo();
demo.printDefaults();
demo.localVariableExample();
}
}
Java enforces specific rules about what constitutes a valid variable name. If you break these rules, your code will not compile.
Rule 1: Variable names can contain letters, digits, underscores (_), and dollar signs ($).
// Valid names using allowed characters int age = 25; int _count = 10; int $price = 99; int item2 = 5; // Invalid - contains special characters that are not allowed // int my-variable = 1; // ERROR: '-' is not allowed // int my variable = 1; // ERROR: spaces are not allowed // int my@name = 1; // ERROR: '@' is not allowed
Rule 2: Variable names must begin with a letter, underscore, or dollar sign. They cannot start with a digit.
// Valid - starts with a letter, underscore, or dollar sign int age = 25; int _hidden = 0; int $amount = 100; // Invalid - starts with a digit // int 10things = 10; // ERROR: cannot start with a digit // int 1stPlace = 1; // ERROR: cannot start with a digit
Rule 3: Variable names are case-sensitive. The variables firstName and firstname are two completely different variables.
String firstName = "Folau"; // 'N' is uppercase String firstname = "Lisa"; // 'n' is lowercase // These are two separate variables System.out.println(firstName); // Output: Folau System.out.println(firstname); // Output: Lisa
Rule 4: Variable names cannot be Java reserved keywords. Words like int, double, class, static, public, return, and about 50 others are reserved for the language itself.
// Invalid - these are reserved keywords // int class = 5; // ERROR: 'class' is a keyword // int static = 10; // ERROR: 'static' is a keyword // int return = 0; // ERROR: 'return' is a keyword // String double = ""; // ERROR: 'double' is a keyword // Valid - similar but not identical to keywords int classCount = 5; int staticValue = 10; String returnMessage = "success";
Rule 5: Variable names cannot contain whitespace.
// Invalid - contains spaces // String first name = "Folau"; // ERROR: spaces not allowed // Valid alternatives String firstName = "Folau"; // camelCase (preferred in Java) String first_name = "Folau"; // snake_case (works but not standard Java style)
Beyond the rules the compiler enforces, the Java community follows strong naming conventions. These are not required for your code to compile, but following them makes your code professional and readable. In a team environment, consistent naming is expected.
In Java, the standard convention is camelCase: start with a lowercase letter, and capitalize the first letter of each subsequent word. This is different from Python and C, which commonly use snake_case.
// Correct - camelCase (Java convention) String firstName = "Folau"; int numberOfStudents = 42; double accountBalance = 1500.00; boolean isLoggedIn = true; // Avoid - snake_case is not standard Java style // String first_name = "Folau"; // int number_of_students = 42;
Variables declared with static final should use all uppercase letters with underscores separating words.
// Correct - UPPER_SNAKE_CASE for constants public static final int MAX_RETRY_COUNT = 3; public static final String BASE_URL = "https://lovemesomecoding.com"; public static final double TAX_RATE = 0.08; // Incorrect - inconsistent style // public static final int maxRetryCount = 3; // looks like a regular variable // public static final int Max_Retry_Count = 3; // mixed style
Great developers write code that reads like a story. A variable name should tell you exactly what it represents without needing a comment. Do not abbreviate unless the abbreviation is universally understood (like "id" or "url").
// Good - clear and descriptive int numberOfStudents = 30; double monthlyPayment = 1200.50; String customerEmailAddress = "folau@example.com"; boolean hasActiveSubscription = true; List<String> pendingOrderIds = new ArrayList<>(); // Bad - vague, abbreviated, or lazy int n = 30; // What does "n" mean? double mp = 1200.50; // Unclear abbreviation String cea = ""; // Unreadable boolean flag = true; // Flag for what? List<String> list = new ArrayList<>(); // List of what?
Boolean variables should read like a yes/no question. Start them with is, has, can, should, or similar prefixes.
// Good - reads naturally as a true/false question boolean isActive = true; boolean hasPermission = false; boolean canDelete = true; boolean shouldRetry = false; boolean isEligibleForDiscount = true; // Bad - unclear whether it is a boolean or something else boolean active = true; // "active" could be a noun boolean permission = false; // sounds like it holds a permission object boolean delete = true; // sounds like a verb/action
Whatever naming style you choose, stick with it throughout your entire project. If you name one constant NUMBER_OF_PERSONS, do not name the next one Number_Of_Rocks. Consistency makes your codebase predictable and easy to navigate.
Remember: code is written for humans to read. Reading code should be like reading a story. It should flow and make sense. Your variable names are one of the strongest signals of how well you understand the program you are building.
After years of writing Java professionally, here are the mistakes I see most often with variables, along with the practices that strong developers follow.
public void processOrder() {
double total;
// MISTAKE: using "total" before assigning a value
// System.out.println(total); // ERROR: variable total might not have been initialized
// FIX: always initialize local variables
total = 0.0;
System.out.println(total); // Output: 0.0
}
Shadowing happens when a local variable has the same name as an instance variable. This can cause subtle bugs because you might think you are modifying the instance variable when you are actually modifying a local copy.
public class ShadowingExample {
private String name = "Default";
public void setName(String name) {
// MISTAKE: this assigns the parameter to itself, not the instance variable
// name = name; // Does nothing useful!
// FIX: use "this" to refer to the instance variable
this.name = name;
}
public static void main(String[] args) {
ShadowingExample example = new ShadowingExample();
example.setName("Folau");
System.out.println(example.name); // Output: Folau
}
}
Declare variables as close as possible to where they are first used, and in the narrowest scope possible. This makes your code easier to follow and reduces the chance of accidental misuse.
// Bad - declared far from where it is used
public void processOrders(List<Order> orders) {
String status; // Declared here...
double discount; // ...but used 20 lines later
for (Order order : orders) {
// ... lots of code ...
}
status = "complete";
discount = 0.10;
}
// Good - declared right where it is needed
public void processOrders(List<Order> orders) {
for (Order order : orders) {
// ... lots of code ...
}
String status = "complete";
double discount = 0.10;
}
A "magic number" is a raw numeric value in your code with no explanation. Replace them with named constants so the code is self-documenting.
// Bad - what does 5 mean? What does 0.08 mean?
if (loginAttempts > 5) {
lockAccount();
}
double tax = subtotal * 0.08;
// Good - the constants explain themselves
public static final int MAX_LOGIN_ATTEMPTS = 5;
public static final double SALES_TAX_RATE = 0.08;
if (loginAttempts > MAX_LOGIN_ATTEMPTS) {
lockAccount();
}
double tax = subtotal * SALES_TAX_RATE;
1. Always initialize local variables. Do not rely on remembering to set them later.
2. Use the narrowest scope possible. If a variable is only needed inside a loop, declare it inside the loop.
3. Prefer final when a variable should not change. This communicates intent and prevents accidental reassignment.
4. Use meaningful, descriptive names. Spend an extra 10 seconds naming a variable well. It will save you and your teammates hours of confusion later.
5. Follow Java naming conventions. camelCase for variables and methods, UPPER_SNAKE_CASE for constants, PascalCase for class names.
6. Replace magic numbers with named constants. Your future self will thank you.
7. One variable, one purpose. Do not reuse a variable for a different purpose partway through a method. Create a new variable with a descriptive name instead.
Here is a complete, runnable program that demonstrates every concept covered in this tutorial.
public class JavaVariablesTutorial {
// ---- Static (class) variables ----
private static int instanceCount = 0;
public static final String APP_NAME = "LoveMeSomeCoding";
public static final int MAX_USERS = 1000;
// ---- Instance variables (default values if not set) ----
private String username;
private int loginAttempts;
private boolean isActive;
// Constructor
public JavaVariablesTutorial(String username) {
this.username = username;
this.isActive = true;
instanceCount++;
}
// Method demonstrating local variables and scope
public void displayProfile() {
// Local variable - must be initialized
String greeting = "Welcome back, " + username + "!";
System.out.println(greeting);
System.out.println("Active: " + isActive);
System.out.println("Login attempts: " + loginAttempts); // default value: 0
// Block scope
if (isActive) {
String status = "Status: Online";
System.out.println(status);
}
// "status" is no longer accessible here
// var keyword (Java 10+)
var totalUsers = instanceCount;
System.out.println("Total users created: " + totalUsers);
}
// Method demonstrating final (local constant)
public boolean canLogin() {
final int maxAttempts = 5;
return loginAttempts < maxAttempts;
}
public void attemptLogin() {
if (canLogin()) {
loginAttempts++;
System.out.println(username + " logged in. Attempts: " + loginAttempts);
} else {
System.out.println(username + " is locked out!");
}
}
public static void main(String[] args) {
System.out.println("App: " + APP_NAME);
System.out.println("Max users allowed: " + MAX_USERS);
System.out.println("---");
// Create instances - each has its own instance variables
JavaVariablesTutorial user1 = new JavaVariablesTutorial("Folau");
JavaVariablesTutorial user2 = new JavaVariablesTutorial("Lisa");
user1.displayProfile();
System.out.println("---");
user2.displayProfile();
System.out.println("---");
// Demonstrate login attempts (instance variable tracks per user)
user1.attemptLogin();
user1.attemptLogin();
user2.attemptLogin();
System.out.println("---");
System.out.println("Total instances created: " + instanceCount);
}
}
/*
* Output:
* App: LoveMeSomeCoding
* Max users allowed: 1000
* ---
* Welcome back, Folau!
* Active: true
* Login attempts: 0
* Status: Online
* Total users created: 2
* ---
* Welcome back, Lisa!
* Active: true
* Login attempts: 0
* Status: Online
* Total users created: 2
* ---
* Folau logged in. Attempts: 1
* Folau logged in. Attempts: 2
* Lisa logged in. Attempts: 1
* ---
* Total instances created: 2
*/
Variables are the foundation of every Java program. Here is what you should take away from this tutorial:
A variable is a named memory location with a type, a name, and a value.
Local variables live inside methods and must be initialized before use. Instance variables belong to objects and get default values. Static variables are shared across all instances of a class.
Scope determines where a variable is visible: block, method, or class level. Always use the narrowest scope possible.
The var keyword (Java 10+) lets the compiler infer the type of local variables from their initializer.
The final keyword makes a variable a constant that cannot be reassigned.
Follow naming conventions consistently: camelCase for variables, UPPER_SNAKE_CASE for constants. Choose clear, descriptive names that make your code read like a well-written story.
Master these fundamentals and you will have a rock-solid foundation for everything else in Java.
A notification system has already become a very popular feature for many applications in recent years. A notification alerts a user with important information like breaking news, product updates, events, offerings, etc. It has become an indispensable part of our daily life. In this chapter, you are asked to design a notification system.
A notification is more than just mobile push notification. Three types of notification formats are: mobile push notification, SMS message, and Email. Figure 10-1 shows an example of each of these notifications.

Solution

Service 1 to N: They represent different services that send notifications via APIs provided by notification servers.
Notification servers: They provide the following functionalities:
Cache: User info, device info, notification templates are cached.
DB: It stores data about user, notification, settings, etc.
Message queues: They remove dependencies between components. Message queues serve as buffers when high volumes of notifications are to be sent out. Each notification type is assigned with a distinct message queue so an outage in one third-party service will not affect other notification types.
Workers: Workers are a list of servers that pull notification events from message queues and send them to the corresponding third-party services.
Third-party services: Already explained in the initial design.
iOS, Android, SMS, Email: Already explained in the initial design.
Now, let us examine how every component works together to send a notification:
How to prevent data loss?
One of the most important requirements in a notification system is that it cannot lose data. Notifications can usually be delayed or re-ordered, but never lost. To satisfy this requirement, the notification system persists notification data in a database and implements a retry mechanism.
Will recipients receive a notification exactly once?
The short answer is no. Although notification is delivered exactly once most of the time, the distributed nature could result in duplicate notifications. To reduce the duplication occurrence, we introduce a dedupe mechanism and handle each failure case carefully. Here is a simple dedupe logic:
When a notification event first arrives, we check if it is seen before by checking the event ID. If it is seen before, it is discarded. Otherwise, we will send out the notification.
Notification template
A large notification system sends out millions of notifications per day, and many of these notifications follow a similar format. Notification templates are introduced to avoid building every notification from scratch. A notification template is a preformatted notification to create your unique notification by customizing parameters, styling, tracking links, etc. Here is an example template of push notifications.
BODY: You dreamed of it. We dared it. [ITEM NAME] is back — only until [DATE]. CTA: Order Now. Or, Save My [ITEM NAME] The benefits of using notification templates include maintaining a consistent format, reducing the margin error, and saving time.
Notification setting
Users generally receive way too many notifications daily and they can easily feel overwhelmed. Thus, many websites and apps give users fine-grained control over notification settings. This information is stored in the notification setting table, with the following fields:
user_id bigInt channel varchar # push notification, email or SMS opt_in boolean # opt-in to receive notification
Before any notification is sent to a user, we first check if a user is opted-in to receive this type of notification.
Rate limiting
To avoid overwhelming users with too many notifications, we can limit the number of notifications a user can receive. This is important because receivers could turn off notifications completely if we send too often.
Retry mechanism
When a third-party service fails to send a notification, the notification will be added to the message queue for retrying. If the problem persists, an alert will be sent out to developers.
Here is the final design, many new components are added in comparison with the previous design.

What is system design?
System design is the process of designing the elements of a system such as the architecture, modules and components, the different interfaces of those components and the data that goes through that system. It is meant to satisfy specific needs and requirements of a business or organization through the engineering of a coherent and well-running system.
What is the purpose of system design
The purpose of the System Design process is to provide sufficient detailed data and information about the system and its system elements to enable the implementation consistent with architectural entities as defined in models and views of the system architecture.
What are the elements of a system?
How do you design a system?
(we are using a chat app as an example)



For endpoints
Users: create, update
Messages: message, timeCreated, destination, etc
For data model
User: firstName, lastName, displayName, email, password, etc
Group: id, list of users
Also talk about which programming language, framework, cloud infrastructure, etc to use.
You also walk through a happy path of how a user uses the system.
Write a function that takes in an array of positive integers and returns the maximum sum of non-adjacent elements in the array.
If the input array is empty, the function should return 0.
Sample input
[75, 105, 120, 75, 90, 135]
Sample output
330 = 75 + 120 + 135
Solution
Time Complexity: O(n)
Space Complexity: O(1)
static int maxSubsetSumNoAdjacent(int[] array) {
int answer = 0;
if (array.length == 0) {
return 0;
} else if (array.length == 1) {
return array[0];
}
int first = Math.max(array[0], array[1]);
int second = array[0];
int current = 0;
for (int i = 2; i < array.length; i++) {
current = Math.max(first, second + array[i]);
second = first;
first = current;
}
answer = first;
return answer;
}