Java: Understanding Primitive Types and Wrapper Objects

What is a primitive type and a wrapper object in Java? How does the compiler handle conversion between the two? When should you use a primitive type or a wrapper object?

Brian Norlander
7 min readMar 19, 2019
Image by NegativeSpace by Food

Primitive Types

Java defines eight primitive data types: byte, short, int, long, float, double, boolean and char. All other variables in java are object reference types.

Primitive types in Java are called literals. A literal is the source code representation of a fixed value in memory. Each primitive type varies in its size and the way in which it is stored.

Primitive Data Type Sizes

+-----------+---------+-------------------------+
| Data Type | Size | Range |
+-----------+---------+-------------------------+
| byte | 1 byte | -128 to 127 |
| short | 2 bytes | -32,768 to 32,767 |
| int | 4 bytes | -2^31 to 2^31-1 |
| long | 8 bytes | -2^63 to 2^63-1 |
| float | 4 bytes | -3.4e38 to 3.4e38 |
| double | 8 bytes | -1.7e308 to 1.7e308 |
| boolean | 1 bit* | true or false |
| char | 2 bytes | '\u0000' to '\uffff' |
+-----------+---------+-------------------------+

*Although the boolean data type represents one bit, it’s size in memory isn’t something that’s precisely defined.

Primitive types in Java are statically-typed, which means all variables must first be declared before they can be used, unlike languages like Python. The following will cause an error:

number = 10;

Defining Primitive Data Types

Here are some examples of how to define primitive data types in Java.

byte b = '\u0045';
short s = 5;
int i = 10;
long l = 100L;
float f = 3.1415f;
double d = 500.25d;
boolean bool = false;
char c = 'A';

In Java, unlike other languages like C++, the primitive data types have default values if they are not initialized. Generally the default values will be zero or null but relying on this is considered bad programming style.

The following will not cause an error when compiling:

public class Dog {
private int age;
public getAge() {
return this.age;
}
Dog() {}
}
public class Example {
public static void main(String[] args) {
Dog d = new Dog();
System.out.println(d.getAge());
}
}

Primitive Data Type Default Values

When an object is created that has primitive type fields, if their values are not defined they will receive default values.

+-----------+---------------+
| Data Type | Default Value |
+-----------+---------------+
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
| boolean | false |
| char | '\u0000' |
+-----------+---------------+

Wrapper Classes

A wrapper class is an object that encapsulates a primitive type. Each primitive type has a corresponding wrapper:

  • byte, short, int, long, float, double, boolean, char
  • Byte, Short, Integer, Long, Float, Double, Boolean, Character
Integer number = 3;
Inheritance tree for primitive types

Each wrapper class has Object as a superclass. Byte, Short, Integer, Long Float and Double have Number as their direct superclass. This means that each wrapper class can implement the methods of the Object class such as hashCode(), equals(Object obj), clone(), and toString().

Wrapper Class Default Values

Since wrapper objects are reference types, their default value will be null. On the other hand, primitive types can never be null. If primitive types are not assigned a value, the language will assign them a default value. This behavior is important to understand when you are choose between using a primitive type or a wrapper object.

Wrapper Objects are Immutable

All primitive wrapper objects in Java are final, which means they are immutable. When a wrapper object get its value modified, the compiler must create a new object and then reassign that object to the original.

public void addOne(Integer i) {
i = i + 1;
}

This creation and eventual garbage collection of objects will add a lot of overhead, especially when doing large computations in loops.

Before we discuss when to use primitive types vs. wrapper classes we must first understand Java’s Autoboxing and Unboxing.

Autoboxing

Introduced in Java 5.0, Autoboxing is the automatic conversion of primitive types to their corresponding object wrapper classes.

List<Integer> numbers = new ArrayList<Integer>();for(int i = 0; i < 10; i++) {
numbers.add(i);
}

Since primitive types cannot be used in Collections or Generics, each time i is added to numbers a new Integer object is created.

Unboxing

Likewise, unboxing is the automatic conversion of object wrapper types into their corresponding primitive types.

Integer a = new Integer(5);
int b = a;

The compiler recognizes that unboxing is needed and does it automatically for you. Be careful, this automatic conversion can cause performance issues and unexpected behavior.

Dangers of Autoboxing and Unboxing

Performance

Autoboxing and Unboxing can cause performance to suffer by creating intermediate objects which creates more work for the Garbage Collector.

Below is an example of how autoboxing and unboxing creates many unnecessary objects:

public class Example {  private static Integer count = 0;  public static void main(String[] args) {    for(int i = 0; i < 1000; ++i) {
count += 1;
}
}
}

Let’s examine what the compiler does behind the scenes.

count += 1 is transformed into count = count + 1.

  • Since count is a wrapper type, it must be unboxed into it’s int value.
  • The int value of count is added to 1 to produce a new int.
  • The new primitive type is assigned back to count, which is a wrapper type, so the compiler creates a new Integer object to assign back to the variable count.

This loop will create 1000 Integer objects in memory. Those objects will need to be garbage collected later. This will have a severe impact on the performance of your program.

In the Javadocs it says:

It is not appropriate to use autoboxing and unboxing for scientific computing, or other performance-sensitive numerical code. An Integer is not a substitute for an int; autoboxing and unboxing blur the distinction between primitive types and reference types, but they do not eliminate it.

Arithmetic and Comparison Operators

When arithmetic and comparison operators are used, autoboxing and unboxing will always happen.

Integer x = new Integer(5);
if (x > 0) {
System.out.println("The number is greater than 0.");
}

The variable x is unboxed into it’s primitive int form and then compared to the int value of 0.

Confusing Equals Behavior

Autoboxing and Unboxing does not always happen with the “==” operator.

When arguments on both sides of the “==” operator are objects, no unboxing will occur and the “==” operator will compare the object’s reference in memory.

public class Example {
public static void compare(Integer a, Integer b) {
if (a == b) {
System.out.println("a and b are equal");
}
else {
System.out.println("a and b are not equal");
}
}
public static void main(String[] args) {
int a = 1000;
int b = 1000;
compare(a,b);
}
}

This program will output a and b are not equal. In the formal parameter of compare, the int values of a and b are autoboxed into new Integer wrapper objects. When the variables are compared, the “==” operator is comparing their object reference in memory.

When only one of the arguments of the “==” operator is an object and the other is a primitive type, unboxing will occur.

public class Example {
public static void compare(Integer a, int b) {
if (a == b) {
System.out.println("a and b are equal");
}
else {
System.out.println("a and b are not equal");
}
}
public static void main(String[] args) {
int a = 1000;
int b = 1000;
compare(a,b);
}
}

This program will output a and b are equal. The wrapper object a is unboxed and it’s int value is compared to the int value of b.

To avoid situations like this we can use the .equals() operator.

public class Example {
public static void compare(Integer a, Integer b) {
if (a.equals(b)) {
System.out.println("a and b are equal");
}
else {
System.out.println("a and b are not equal");
}
}
public static void main(String[] args) {
int a = 1000;
int b = 1000;
compare(a,b);
}
}

This will output a and b are equal.

When to Use Primitive Types

  • When doing a large amount of calculations, primitive types are always faster — they have much less overhead.
  • When you don’t want the variable to be able to be null.
  • When you don’t want the default value to be null.
  • If the method must return a value

When to Use Wrapper Class

  • When you are using Collections or Generics — it is required
  • If you want the MIN_SIZE or MAX_SIZE of a type.
  • When you want the variable to be able to be null.
  • When you want to default value to be null.
  • If sometimes the method can return a null value.

Verdict

Generally, choose primitive types over wrapper classes unless using a wrapper class is necessary. Primitive Types will never be slower than Wrapper Objects, however Wrapper Objects have the advantage of being able to be null.

When choosing between using a primitive type or a wrapper class it will always depend on your situation. Many programmers have different opinions on this topic and it is certainly not a settled debate.

--

--