Data types in Java with examples
-
Last Updated: July 1, 2023
-
By: javahandson
-
Series
Learn Java in a easy way
Every value you write in a Java program has a type, and picking that type is the very first decision you make. That is what data types in Java are about: telling the compiler what kind of value a variable holds, and how much memory to hand it. Get this right and everything else clicks into place. Get it wrong and your number silently wraps around, your decimal loses precision, or your code refuses to compile at all.
Think about a set of storage boxes in your kitchen. A tiny box holds a pinch of salt. A big box holds five kilos of rice. You would never buy a huge crate just to store one spoon of sugar.
Memory works the same way. A variable needs a box, and the type decides how big that box will be. Ask for a byte and Java hands you a tiny 1-byte box. Ask for a long and you get a roomy 8-byte one.
There is a second job too. The type tells the compiler what you can legally do with the value. You may divide two numbers. You may not divide two booleans. Java catches that nonsense before your program ever runs.
So a type gives you three things at once: a size, a range of allowed values, and a set of allowed operations. That is the whole idea in one sentence.
int result = 50; // 4 bytes, whole number char grade = 'A'; // 2 bytes, one character boolean pass = true; // logical flag
Notice how each line reads almost like English. The type comes first, then the name, then the value. Java never guesses for you, and that strictness is a feature, not a chore.
We will start simple and build up. By the end you should be able to look at any variable and know exactly what it costs and what it can hold.
char needs 2 bytes while C gets away with 1varNo prior Java needed. If you can read a System.out.println, you are ready.
Java splits every type into two camps. Primitives come baked into the language. Non-primitives you build, or the library builds for you.
Primitives are the atoms. Java ships exactly eight of them, and that list has never grown. You cannot invent a ninth one.
Non-primitives are the molecules. String, arrays, classes, interfaces and enums all live here. Each one sits on top of primitives, or on top of other non-primitives.
// primitive - the value lives right in the variable
int age = 25;
// non-primitive - the variable holds a reference to an object
String name = "Riya";
int[] marks = {90, 85, 78};The split is not just naming. It changes how your program behaves at runtime.
null. Any non-primitive can..length() on a String and it answers; try that on an int and the compiler laughs.That last point saves real time when you read unfamiliar code. Spot int and you know the value is right there. Spot Integer and you know an object is involved.
Four primitives hold whole numbers. They differ only in how many bytes they take, and therefore how far they can count. All four are signed, so each one covers negatives as well.
The smallest of them all. A byte takes 1 byte, which is 8 bits, and its range runs from -128 to 127. Its default value is 0.
Watch that range carefully. Plenty of tutorials print “-127 to 128” and it is simply wrong. The low end is -128 and the high end is 127.
byte temperature = -40; byte volume = 100; System.out.println(temperature + " " + volume); // Output: -40 100 // byte tooBig = 200; // compile error: range is -128 to 127
When would you actually reach for it? Raw file bytes, image pixels, network packets. Anywhere you handle a big pile of small numbers and memory really matters.
Double the space, far more room. A short takes 2 bytes and covers -32,768 to 32,767. Its default value is 0.
short year = 2026; short population = 30000; System.out.println(year + " " + population); // Output: 2026 30000
Honestly, short is rare in modern code. Most developers jump straight to int because the memory saving rarely pays for the extra thinking.
Here is your workhorse. An int takes 4 bytes and spans -2,147,483,648 to 2,147,483,647. Its default value is 0.
Roughly two billion either way. That covers ages, counts, loop counters, IDs and almost everything else you meet day to day.
Java also treats int as the default type for any whole-number literal. Write 5 in your code and the compiler reads it as an int, nothing else.
int salary = 850000; int loss = -2000; int max = 2147483647; System.out.println(max); // Output: 2147483647
When two billion is not enough, go long. A long takes 8 bytes and runs from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Its default value is 0L.
That is about 9.2 quintillion in each direction. Timestamps in milliseconds, view counts on a viral video, national population totals: all comfortable here.
Now the rule that trips everyone. Any big literal still counts as an int first, so you must add an L to mark it as a long.
// long views = 3000000000; // compile error! literal is read as int long views = 3000000000L; // correct - the L saves you long small = 100; // fine, 100 fits in an int and widens quietly System.out.println(views); // Output: 3000000000
Use a capital L, never lowercase. A lowercase l looks exactly like the digit 1 in most fonts, and that has burned many a night shift.
Those ranges are not random numbers someone picked. Each one falls out of a simple formula. For any signed type with n bits, the range looks like this:
Range = -2^(n-1) to 2^(n-1) - 1 where n = total number of bits in the type
Read the exponent carefully: it is n minus 1, not n. One bit goes to the sign, so only n-1 bits remain for the magnitude. Many older tutorials write it as “-2^n to 2^n – 1”, and that formula is flat-out wrong.
Let us plug real numbers in. A byte holds 8 bits, so n equals 8.
byte : n = 8 bits low = -2^(8-1) = -2^7 = -128 high = 2^(8-1) - 1 = 2^7 - 1 = 128 - 1 = 127 Range: -128 to 127 int : n = 32 bits low = -2^31 = -2,147,483,648 high = 2^31 - 1 = 2,147,483,647 Range: -2,147,483,648 to 2,147,483,647 long : n = 64 bits low = -2^63 = -9,223,372,036,854,775,808 high = 2^63 - 1 = 9,223,372,036,854,775,807
Try the same trick on short. Sixteen bits, so 2^15 gives 32,768, and the range lands on -32,768 to 32,767. The pattern never breaks.
Look at those ranges again. The negative end always reaches one step further than the positive end. Why the imbalance?
Blame zero. An 8-bit box holds 256 distinct patterns, and one of them must represent zero. Zero sits on the positive team, so the positives get 0 through 127, which is 128 slots. That leaves 128 slots for the negatives, giving -128 through -1.
Add them up: 128 plus 128 equals 256. Every pattern has a job, and nothing is wasted. Neat, once you see it.
Whole numbers only take you so far. Prices, temperatures, averages and measurements all need a decimal point. Java gives you two types for that job.
A float takes 4 bytes. Its range stretches roughly from -3.4e38 to +3.4e38, and it keeps about 7 decimal digits of precision. Its default value is 0.0f.
Here comes the suffix rule. Java reads every decimal literal as a double by default, so a bare 2.5 will not fit into a float slot. You must append an f.
// float price = 2.5; // compile error: 2.5 is a double float price = 2.5f; // correct float pi = 3.14159f; System.out.println(price + " " + pi); // Output: 2.5 3.14159
Seven digits sounds like plenty until it isn’t. Push past that and float starts rounding on you, quietly.
A double takes 8 bytes. Its range runs roughly from -1.7e308 to +1.7e308, with around 15 decimal digits of precision. Its default value is 0.0d.
Because a decimal literal already counts as a double, the d suffix is optional. Both lines below compile happily.
double distance = 384400.5; // no suffix needed double same = 384400.5d; // d is optional double tiny = 1.0 / 3; System.out.println(tiny); // Output: 0.3333333333333333
Reach for double unless you have a strong reason not to. It doubles your precision for a cost most programs never notice.
Both types store binary fractions, and some ordinary decimals have no exact binary form. So 0.1 plus 0.2 does not land on 0.3.
System.out.println(0.1 + 0.2); // Output: 0.30000000000000004
Shocking the first time you see it, right? Yet nothing is broken here. Binary simply cannot express 0.1 perfectly, exactly as decimal cannot express one third.
For prices, invoices and interest, switch to java.math.BigDecimal. Your accountant will thank you.
A char holds exactly one character. It takes 2 bytes and its range runs from 0 to 65,535, covering the Unicode character set. Its default value is '\u0000', the null character, which prints as blank space.
Single quotes matter here. One character in single quotes gives a char; text in double quotes gives a String.
char grade = 'A'; char symbol = '#'; char rupee = '₹'; // Unicode escape System.out.println(grade + " " + symbol + " " + rupee); // Output: A # ₹
In C, a char takes 1 byte. Java doubles that. Why?
One byte gives you 2^8, which is 256 slots. ASCII fits in there comfortably: English letters, digits and punctuation. That was fine in 1972.
Java, though, aimed at the whole planet from day one. Hindi, Chinese, Arabic, Greek, Japanese and dozens more all need their own characters. Cram them into 256 slots and you run out almost immediately.
So Java picked Unicode and gave char 2 bytes. Two bytes give 2^16, which is 65,536 slots. Suddenly every major script on Earth has room to breathe.
Under the hood a char is just an unsigned 16-bit number. The letter ‘A’ is really the number 65. Print it as an int and Java shows you the code.
char letter = 'A'; int code = letter; // widening: char to int System.out.println(code); // Output: 65 char next = (char) (letter + 1); System.out.println(next); // Output: B
That trick powers a lot of small utilities. Shifting letters, building ciphers, checking whether a character is a digit: all of it leans on char arithmetic.
The simplest primitive of the eight. A boolean holds only true or false, and its default value is false.
Every if, every while, every comparison in your code produces one of these. It is the switch that steers your whole program.
boolean isActive = true;
boolean hasPassed = (marks >= 40);
if (isActive) {
System.out.println("User is live"); // Output: User is live
}One warning for anyone arriving from C or Python. Java refuses to treat 1 as true or 0 as false. Write if (1) and the compiler stops you cold.
Great interview question, and the honest answer surprises people. The Java spec never fixes a size for boolean.
Logically it needs a single bit. In practice, though, a JVM usually spends a whole byte on it, because CPUs address bytes and not bits. Inside an array, some JVMs pack them tighter.
So the correct answer is: JVM-dependent. Say “1 bit of information, but the actual footprint depends on the JVM” and you will sound like someone who has read the spec.
Here is the whole family in one place. Bookmark this table; you will come back to it more often than you expect.
| Type | Size | Range | Default | Example |
|---|---|---|---|---|
byte |
1 byte | -128 to 127 | 0 | byte b = 100; |
short |
2 bytes | -32,768 to 32,767 | 0 | short s = 30000; |
int |
4 bytes | -2,147,483,648 to 2,147,483,647 | 0 | int i = 100000; |
long |
8 bytes | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | 0L | long l = 3000000000L; |
float |
4 bytes | approx. -3.4e38 to 3.4e38 (7 decimal digits) | 0.0f | float f = 2.5f; |
double |
8 bytes | approx. -1.7e308 to 1.7e308 (15 decimal digits) | 0.0d | double d = 2.5; |
char |
2 bytes | 0 to 65,535 (Unicode) | ‘\u0000’ | char c = 'A'; |
boolean |
JVM-dependent | true or false | false | boolean ok = true; |
Four integer types, two floating-point types, one character type, one logical type. Eight in total, and that number never changes.
Declare a field inside a class and skip the value. Java does not complain. Instead, the JVM quietly fills it with a default.
Numbers start at zero. Booleans start at false. Characters start at the null character. Objects, including String, start at null.
Do not take my word for it. Run this and watch the JVM fill every slot for you.
package com.javahandson;
public class DefaultValues {
byte b;
short s;
int i;
long l;
float f;
double d;
char ch;
boolean bool;
String name;
public static void main(String[] args) {
DefaultValues dv = new DefaultValues();
System.out.println("byte : " + dv.b);
System.out.println("short : " + dv.s);
System.out.println("int : " + dv.i);
System.out.println("long : " + dv.l);
System.out.println("float : " + dv.f);
System.out.println("double : " + dv.d);
System.out.println("char : [" + dv.ch + "]");
System.out.println("boolean : " + dv.bool);
System.out.println("String : " + dv.name);
}
}
/* Output:
byte : 0
short : 0
int : 0
long : 0
float : 0.0
double : 0.0
char : [ ]
boolean : false
String : null
*/See the char line? Square brackets wrap it so the blank becomes visible. That blank is '\u0000', not a space you typed.
Also notice String printing null, not an empty string. Non-primitives default to null every single time, and mixing that up causes real bugs.
Now the point most tutorials skip, and interviewers absolutely love. Local variables get no default value at all.
A local variable lives inside a method. The JVM does not initialise it, so you must assign a value before you read it. Skip that step and the compiler refuses to build.
public void demo() {
int count;
// System.out.println(count);
// compile error: variable count might not have been initialized
count = 10;
System.out.println(count); // Output: 10
}Why the difference? Fields belong to an object, and the JVM zeroes that whole block of memory when it creates the object. Locals live on the stack, where nobody sweeps up for you.
Honestly, this rule is a gift. It turns a whole class of “uninitialised value” bugs into a compile error you fix in ten seconds.
Primitives hold one value each. Real programs need more than that, so Java gives you non-primitive types to group and model things.
A String holds a sequence of characters. It behaves so naturally that beginners often assume it must be a primitive. It isn’t.
String city = "Pune"; System.out.println(city.length()); // Output: 4 System.out.println(city.toUpperCase()); // Output: PUNE System.out.println(city.charAt(0)); // Output: P
Those dots give the game away. Primitives have no methods, yet String is packed with them.
String is a class in java.lang. It extends Object, exactly like every other class you write. Only a class can do that.
length(), concat() and contains().Object, which primitives can never do.null, not some empty character.Java does grant it two special favours, and that is where the confusion starts. You may create one with a literal, "Pune", and you may join two with +. No other class gets that treatment. Underneath, though, it remains an ordinary object.
An array groups many values of one type under a single name. Older textbooks call this a derived type, because it derives directly from a primitive or a class.
Arrays are homogeneous. Every slot must share the same type, no exceptions.
int[] marks = {20, 30, 70, 80}; // valid - all ints
System.out.println(marks[2]); // Output: 70
System.out.println(marks.length); // Output: 4
// int[] mixed = {20, "Java", true}; // invalid - mixed typesC and C++ also offer structs, unions and pointers as derived types. Java dropped all three on purpose, so arrays are the only derived type left standing.
A class lets you invent a type Java never shipped. Need a Student? Build one. Need an Invoice or a Booking? Same story.
Unlike an array, a class can mix types freely. Bundle an int, a String and a boolean into one tidy package.
package com.javahandson.oops;
public class Student {
int studentId;
String name;
String standard;
public int getStudentId() {
return studentId;
}
public void setStudentId(int studentId) {
this.studentId = studentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStandard() {
return standard;
}
public void setStandard(String standard) {
this.standard = standard;
}
}Now Student works like any other type. Declare it, pass it to methods, drop it into a list. You just extended the language.
Student s = new Student();
s.setStudentId(101);
s.setName("Riya");
System.out.println(s.getName()); // Output: RiyaTwo more non-primitives round out the set.
An interface names a contract without any code behind it. Say Runnable and everyone knows a run() method exists. It is still a type, so you can declare variables of it.
An enum pins a variable to a fixed list of constants. Days of the week, order status, payment mode: all perfect fits.
enum Status { ACTIVE, INACTIVE, BLOCKED }
Status current = Status.ACTIVE;
System.out.println(current); // Output: ACTIVESooner or later you need a value to hop from one type to another. That hop is casting, and it comes in two flavours.
Pouring a small glass into a big jug is safe. Nothing spills. Java feels the same way about moving a small type into a bigger one, so it does the job for you.
byte -> short -> int -> long -> float -> double
char -> int -> long -> float -> doubleint i = 100; long l = i; // int widens to long, no cast written double d = l; // long widens to double System.out.println(l); // Output: 100 System.out.println(d); // Output: 100.0
Widening is automatic and lossless for whole numbers. You write no extra syntax, and Java promises the value survives intact.
The other direction is risky. Pour a jug into a glass and it overflows. Java therefore refuses to do it silently, and demands a cast in brackets.
double price = 99.99; int rounded = (int) price; // explicit cast System.out.println(rounded); // Output: 99 (decimal chopped off) long big = 130L; byte b = (byte) big; System.out.println(b); // Output: -126 (wrapped around!)
Read that cast as a signed note: “I know this may lose data, and I accept it.” Java trusts you and moves on.
Two very different things can go wrong, so keep them straight.
double to int chops off the decimals. It does not round. So 99.99 lands on 99, and -2.7 lands on -2.long to byte keeps only the lowest 8 bits and throws the rest away. That is why 130 came back as -126.Where did -126 come from? The value 130 in 8 bits is 10000010. Byte reads the leading 1 as a negative sign, and the pattern means -126. Precision does not shrink here; the number itself changes meaning.
Here is the distinction that separates a beginner from someone who has debugged production code.
Write a literal outside the range and the compiler catches it. Nothing runs; you fix the line and move on.
// byte b = 200; // compile error: incompatible types, possible lossy conversion from int to byte
Compute your way past the range at runtime, though, and Java says nothing. No exception, no warning, no crash. The value simply wraps around and your program carries on with garbage.
Picture the range as a clock face. Step one past the top and you land at the bottom, exactly like 12 rolling over to 1.
byte b = 127; b++; System.out.println(b); // Output: -128 (wrapped, no error) int max = Integer.MAX_VALUE; // 2,147,483,647 System.out.println(max + 1); // Output: -2147483648 int a = 100000; int product = a * a; // 10,000,000,000 does not fit in an int System.out.println(product); // Output: 1410065408 (silent garbage)
That last one is genuinely dangerous. Both operands are ints, so Java multiplies them as ints and overflows before anything is copied anywhere. Assigning to a long afterwards cannot rescue it.
long.long product = (long) a * a;Math.addExact() or Math.multiplyExact(), which throw an exception instead of wrapping.BigInteger when even a long runs out of room.int a = 100000; long safe = (long) a * a; System.out.println(safe); // Output: 10000000000 // Math.multiplyExact(a, a); // throws ArithmeticException: integer overflow
Every primitive has an object twin, called a wrapper class. Think of the wrapper as a gift box: same value inside, but now it counts as an object.
| Primitive | Wrapper |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
Why bother? Collections refuse primitives. You cannot write List<int>, so you write List<Integer> instead.
Wrappers also accept null, which primitives cannot. A missing age reads naturally as a null Integer, while an int would have to fake it with 0 or -1.
Since Java 5 the compiler swaps between the two forms on its own. Boxing wraps a primitive into an object; unboxing unwraps it again.
List<Integer> scores = new ArrayList<>(); scores.add(90); // autoboxing: int 90 becomes Integer int first = scores.get(0); // unboxing: Integer becomes int System.out.println(first); // Output: 90 Integer boxed = 5; // autoboxing int plain = boxed + 1; // unboxing, then add System.out.println(plain); // Output: 6
Convenient, yes. Free, no. Each box allocates an object, so a tight loop doing millions of boxings will crawl. Keep primitives in hot code.
Unboxing a null wrapper also explodes with a NullPointerException, which is a classic 2 a.m. bug.
Now a favourite interview puzzle. Compare two Integers with == and the answer flips depending on the value.
Integer a = 100, b = 100; System.out.println(a == b); // Output: true Integer x = 200, y = 200; System.out.println(x == y); // Output: false (!) System.out.println(x.equals(y)); // Output: true
Java caches Integer objects from -128 to 127, so 100 reuses one shared object. The value 200 sits outside the cache, so each variable points at a fresh object, and == compares addresses.
The lesson is short. Use == for primitives, and .equals() for objects. Always.
Beginners agonise over this choice. In practice a handful of rules cover almost every case.
int and stop thinking about it.long, especially for timestamps and IDs.double, not float.BigDecimal, never a floating-point type.boolean.char. Many characters? Pick String.byte earn its keep.Notice how rarely short and float appear. Both exist for good historical reasons, yet modern code hardly touches them.
Java 10 added var, and people immediately misread it. No, Java did not turn into JavaScript.
The type is still fixed and still checked. You simply let the compiler infer it from the right-hand side.
var count = 10; // inferred as int var name = "Riya"; // inferred as String var list = new ArrayList<String>(); // inferred as ArrayList<String> // count = "hello"; // compile error - count is locked to int
Three limits are worth remembering. var works on local variables only, never on fields or method parameters. It needs an initialiser on the same line. And var x = null; will not compile, because nothing can be inferred from null.
Use it when the type is obvious from the right side. Skip it when hiding the type would make a reader guess.
Every decimal literal starts life as a double. Drop it into a float without the suffix and the compiler stops you.
// float rate = 7.5; // error: possible lossy conversion from double to float float rate = 7.5f; // fixed
A literal out of range fails at compile time, which is friendly. Arithmetic that drifts out of range fails silently, which is not.
byte b = 127; b += 1; // compound assignment hides an implicit cast System.out.println(b); // Output: -128, and not a single warning
Sneaky detail: b += 1 compiles even though b = b + 1 would not. The compound operator slips in a hidden cast on your behalf.
For primitives, == compares values and behaves exactly as you expect. For objects it compares references instead.
String a = "java";
String b = "java";
String c = new String("java");
System.out.println(a == b); // Output: true (same pooled literal)
System.out.println(a == c); // Output: false (different object)
System.out.println(a.equals(c)); // Output: true (same characters)The first line lulls you into a false sense of safety, because string literals share a pool. Feed the same code a value from a file or a form, and == starts returning false. Just use .equals() everywhere.
Divide an int by an int and you get an int. The decimal part vanishes before any assignment happens.
int total = 7, count = 2; double wrong = total / count; System.out.println(wrong); // Output: 3.0 (not 3.5!) double right = (double) total / count; System.out.println(right); // Output: 3.5
Look closely at the first case. Java divides 7 by 2 as ints, gets 3, and only then widens it to 3.0. Casting one operand first fixes everything.
Averages, percentages and progress bars all fall into this pit. Cast early and you never see it again.
A: Java has exactly eight primitives, in four groups. Integers: byte, short, int, long. Floating-point: float, double. Character: char. Logical: boolean. That list is fixed and has not changed since Java 1.0.
A: A byte spans -128 to 127. The formula for an n-bit signed type is -2^(n-1) to 2^(n-1) – 1. A byte holds 8 bits, so the range works out to -2^7 to 2^7 – 1, which gives -128 to 127. Beware of tutorials printing “-127 to 128”; that is simply wrong.
A: Zero takes one slot on the positive side. An 8-bit type has 256 patterns in total, so the positives cover 0 to 127 (128 slots) and the negatives cover -128 to -1 (another 128 slots). Nothing goes to waste.
A: C relies on ASCII, and 1 byte gives 2^8 = 256 characters, which covers English. Java targets international text, so it adopted Unicode. Two bytes give 2^16 = 65,536 characters, enough room for Hindi, Chinese, Arabic, Greek and many more scripts.
A: byte, short and int default to 0. A long defaults to 0L, a float to 0.0f and a double to 0.0d. A char defaults to ‘\u0000’, the null character, and a boolean defaults to false. Any non-primitive, including String, defaults to null.
A: No, and this trips up nearly everyone. Only fields receive defaults, because the JVM zeroes an object’s memory on creation. Local variables sit on the stack and stay uninitialised, so reading one before you assign it causes a compile error.
A: String is a class in java.lang that extends Object and carries methods such as length(), concat() and contains(). Only a class can extend another class or hold methods. A String variable also stores a reference and defaults to null, so it is predefined but definitely non-primitive.
A: Widening moves a smaller type into a larger one, such as int to long. Java performs it automatically and no data is lost. Narrowing moves a larger type into a smaller one, such as double to int, and it needs an explicit cast because the value may lose precision or wrap around.
A: Nothing dramatic, and that is the danger. The value wraps around silently, so Integer.MAX_VALUE + 1 becomes -2147483648. Java throws no exception. Use a long, cast one operand before the maths, or call Math.addExact() to make the overflow throw.
A: Generics work only with objects, so List<int> does not compile and you write List<Integer> instead. Autoboxing lets the compiler convert an int to an Integer automatically, and unboxing converts it back. Both cost an allocation, so avoid them inside hot loops.
A: float and double store binary fractions, and 0.1 has no exact binary form, just as one third has no exact decimal form. The tiny rounding error surfaces as 0.30000000000000004. For money and any audited figure, use java.math.BigDecimal.
double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 0.30000000000000004
System.out.println((a + b) == 0.3); // false
A: The specification never fixes one. Logically a boolean carries a single bit of information, yet a typical JVM spends a whole byte on it because hardware addresses bytes, not bits. So the honest answer is JVM-dependent.
Let us pull the threads together. A type fixes three things for a variable: its size in memory, the range of values it accepts, and the operations you may perform on it.
Java gives you eight primitives. Four hold whole numbers, two hold decimals, one holds a character, and one holds a truth value. Everything else, including String and arrays, sits in the non-primitive camp and stores a reference instead of a value.
Keep the numbers straight and you are ahead of most beginners. A byte covers -128 to 127, an int covers roughly plus or minus two billion, and the formula behind both is -2^(n-1) to 2^(n-1) – 1.
Watch the small traps too. Add f to float literals and L to long literals. Remember that fields get defaults while locals get none. Compare objects with .equals(), and cast before you divide.
Master this chapter and the rest of Java gets much easier. Variables, methods, arrays and classes all build straight on top of it.