6. Instantiating Types
Table of Contents
- 6.1 Introduction
- 6.2 Default Variable Initialization
- 6.3 Wrapper Types
- 6.4 Equality in Java
6.1 Introduction
In Java, a type can be either a primitive type (such as int, double, boolean, etc.) or a reference type (classes, interfaces, arrays, enums, records, etc.). See: Java Data Types and Casting
The way instances are created depends on the category of the type:
- Primitive types
Instances of primitive types are created simply by declaring a variable.
The JVM automatically allocates memory to hold the value, and no explicit keyword is needed.
int age = 30; // creates a primitive int with value 30
boolean flag = true; // creates a primitive boolean with value true
double pi = 3.14159; // creates a primitive double with value 3.14159
- Reference types (objects)
Instances of class types are created using the new keyword (except for a few special cases such as string literals, records with canonical constructors, or factory methods). The new keyword allocates memory on the heap and invokes a constructor of the class.
String name = new String("Alice"); // creates a new String object explicitly
Person p = new Person(); // creates a new Person object using its constructor
It is also common to rely on literals or factory methods for object creation.
String text = "Hello World";
List<String> list = List.of("A", "B", "C"); // factory method immutabile
Map<String, Integer> map = Map.of("one", 1, "two", 2); // factory method immutabile
Optional<String> opt = Optional.of("value"); // factory method
LocalDate date = LocalDate.of(2025, 3, 15);
Integer boxed = Integer.valueOf(10);
Important
String literals do not require new and are stored in the String pool.
Using new String("x") always creates a new object on the heap.
6.1.1 Handling Primitive Types
6.1.1.1 Declaring a Primitive
Declaring a primitive type (as with reference types) means reserving space in memory for a variable of a given type, without necessarily giving it a value.
Warning
Unlike primitives, whose size depends on their specific type (e.g., int vs long), reference types always occupy the same fixed size in memory — what varies is the size of the object they point to.
- Syntax examples for declaration only:
int number;
boolean active;
char letter;
int x, y, z; // Multiple declarations in one statement: Java allows declaring multiple variables of the same type
Important
The modifiers and the type declared at the beginning of a variable declaration apply to all variables declared in the same statement.
Exception: when declaring arrays using brackets after the variable name, the brackets are part of the individual variable declarator, not the base type.
- Examples
static int a, b, c;
// is equivalent to :
static int a;
static int b;
static int c;
int[] a, b; // both are arrays of int
int c[], d; // only c is an array, d is a regular int
6.1.1.2 Assigning a Primitive
Assigning a primitive type (as with reference types) means storing a value into a declared variable of that given type.
For primitives, the variable holds the value itself, while for reference types the variable holds the memory address (a reference) of the object being pointed to.
- Syntax examples:
int number; // Declaring an int type: a variable called "number"
number = 10; // Assigning the value 10 to this variable
char letter = 'A'; // Declaring and Assigning in a single statement: declaration and assignment can be combined
int a1, a2; // Multiple declarations
int a = 1, b = 2, c = 3; // Multiple declarations & assignements
char b1, b2, b3 = 'C'; // Mixed declarations (2 declarations + 1 assignment)
double d1, double d2; // ERROR - NOT LEGAL
int v1; v2; // ERROR - NOT LEGAL
Important
When you write a number directly in the code (a numeric literal), Java assumes by default that it is of type int.
If the value does not fit into an int, the code will not compile unless you explicitly mark it with the correct suffix.
- Syntax example for a numeric literal:
long exNumLit = 5729685479; // ❌ Does not compile.
// Even though the value would fit in a long,
// a plain numeric literal is assumed to be an int,
// and this number is too large for int.
Changing the declaration adding the correct suffix (L or l) will solve:
long exNumLit = 5729685479L;
or
long exNumLit = 5729685479l;
Declaring a reference type means reserving space in memory for a variable that will contain a reference (pointer) to an object of the specified type.
At this stage, no object is created yet — the variable only has the potential to point to one.
Warning
Unlike primitives, whose size depends on their specific type (e.g., int vs long), reference variables always occupy the same fixed size in memory (enough to store a reference).
What varies is the size of the object they point to, which is allocated separately on the heap.
- Syntax examples for declaration only:
String name;
Person person;
List<Integer> numbers;
Person p1, p2, p3; // Multiple declarations in one statement
String a = "abc", b = "def", c = "ghi"; // Multiple declarations & assignements
String b1, b2, b3 = "abc" // Mixed declarations (b1, b2) with one assignement (b3)
String d1, String d2; // ERROR - NOT LEGAL
String v1; v2; // ERROR - NOT LEGAL
6.1.2 Handling Reference Types
6.1.2.1 Creating and Assigning a Reference
Assigning a reference type means storing into the variable the memory address of an object.
This is normally done after the creation of the object with the new keyword and a Constructor, or by using a literal or a factory method.
A reference can also be assigned to another object of the same or compatible type.
Reference types can also be assigned null, which means that they do not refer to an object.
- Syntax examples:
Person person = new Person(); // Example with 'new' and a constructor 'Person()':
// 'new Person()' creates a new Person object on the heap
// and returns its reference, which is stored in the variable 'person'.
String greeting = "Hello"; // Example with literal (for String).
List<Integer> numbers = List.of(1, 2, 3); // Example with a factory method.
6.1.2.2 Constructors
In the example, Person() is a constructor — a special kind of method used to initialize new objects.
Whenever you call new Person(), the constructor runs and sets up the newly created instance.
Constructors have three main characteristics:
- The constructor name must match the class name exactly (case-sensitive).
- Constructors do not declare a return type (not even
void). - If you do not define any constructor in your class, the compiler will automatically provide a default no-argument constructor that does nothing.
Warning
If you see a method that has the same name as the class but also declares a return type, it is not a constructor. It is simply a regular method (though starting method names with a capital letter is against Java naming conventions).
The purpose of a constructor is to initialize the state of a newly created object — typically by assigning values to its fields, either with default values or using parameters passed to the constructor.
- Example 1: Default constructor (no parameters)
public class Person {
String name;
int age;
// Default constructor
public Person() {
name = "Unknown";
age = 0;
}
}
Person p1 = new Person(); // name = "Unknown", age = 0
- Example 2: Constructor with parameters
public class Person {
String name;
int age;
// Constructor with parameters
public Person(String newName, int newAge) {
name = newName;
age = newAge;
}
}
Person p2 = new Person("Alice", 30); // name = "Alice", age = 30
- Example 3: Multiple constructors (constructor overloading)
public class Person {
String name;
int age;
// Default constructor
public Person() {
this("Unknown", 0); // calls the other constructor
}
// Constructor with parameters
public Person(String newName, int newAge) {
name = newName;
age = newAge;
}
}
Person p1 = new Person(); // name = "Unknown", age = 0
Person p2 = new Person("Bob", 25); // name = "Bob", age = 25
Important
- Constructors are not inherited: if a superclass defines constructors, they are not automatically available in the subclass — you must declare them explicitly.
- If you declare any constructor in a class, the compiler does not generate the default no-argument constructor: if you still need a no-argument constructor, you must declare it manually.
6.1.2.3 Instance Initializer Blocks
In addition to constructors, Java provides a mechanism called initializer blocks to help initialize objects.
These are blocks of code inside a class, enclosed in { }, that run every time an instance is created, just before the constructor body is executed.
Characteristics
- Also called instance initializer blocks.
- Executed, along with fields initializers, in the order in which they appear in the class definition but always before Constructors.
- Useful when multiple constructors need to share common initialization code.
Example: Using an Instance Initializer Block
public class Person {
String name;
int age;
// Instance initializer block
{
System.out.println("Instance initializer block executed");
age = 18; // default age for every Person
}
// Default constructor
public Person() {
name = "Unknown";
}
// Constructor with parameters
public Person(String newName) {
name = newName;
}
}
Person p1 = new Person(); // prints "Instance initializer block executed"
Person p2 = new Person("Alice"); // prints "Instance initializer block executed"
Note
In this example, the initializer block runs before either constructor body. Both p1 and p2 will start with age = 18, regardless of which constructor is used.
Multiple Initializer Blocks: if a class contains multiple initializer blocks, they are executed in the order they appear in the source file:
- Example:
public class Example {
{
System.out.println("First block");
}
{
System.out.println("Second block");
}
}
Example ex = new Example();
// Output:
// First block
// Second block
Note
Instance initializer blocks are less common in practice, because similar logic can often be placed directly in constructors. It is important to know that: - They always run before the constructor body. - They are executed in the order of declaration in the class. - They can be combined with constructors to avoid code duplication.
Warning
Order of initialization when creating an object 1. Static fields 2. Static initializer blocks 3. Instance fields 4. Instance initializer blocks 5. Constructor body
6.2 Default Variable Initialization
6.2.1 Instance and Class variables
- An instance variable (a field) is a value defined within an instance of an object;
- A class variable (defined with the keyword static) is defined at class level and it is shared among all the objects (instances of the class)
Instance and class variables are given a default value, by the compiler, if not initialized.
- Table of default values for instance & class variables;
| Type | Default Value |
|---|---|
| Object | null |
| Numeric | 0 |
| boolean | false |
| char | '\u0000' (NUL) |
6.2.2 Final Instance Variables
Unlike regular instance and class variables, final variables are not default-initialized by the compiler.
A final variable must be explicitly assigned exactly once, otherwise the code does not compile.
This applies to both:
- final instance variables
- static final class variables
Note
We can assign a null value to a final instance or class instance variables as long they are explicitely set.
Java enforces this rule because a final variable represents a value that must be known and fixed before use.
Final Instance Variables
A final instance variable must be assigned exactly once, and the assignment must occur in one of the following:
- At the point of declaration
- In an instance initializer block
- Inside every constructor
If the class has multiple constructors, the variable must be assigned in all of them.
- Example:
public class Person { final int id; // must be assigned before constructor ends String name; // Constructor 1 public Person(int id, String name) { this.id = id; // ok this.name = name; } // Constructor 2 public Person() { this.id = 0; // also required here this.name = "Unknown"; } }
Warning
Trying to compile without assigning id inside every constructor produces a compile-time error:
variable id might not have been initialized
static final Class Variables (Constants)
A static final variable belongs to the class rather than to any instance.
It must also be assigned exactly once, but assignment can occur in one of the following places:
- At the point of declaration
-
Inside a static initializer block
-
Example:
public class AppConfig { static final int TIMEOUT = 5000; // assigned at declaration static final String VERSION; // assigned in static initializer static { VERSION = "1.0.0"; // ok } }
Attempting to assign a static final in a constructor is illegal.
Key Rules for final Fields
| Scenario | Allowed? | Notes |
|---|---|---|
| Assign at declaration | ✔ | Most common pattern |
| Assign in constructor | ✔ | All constructors must assign it |
| Assign in instance initializer | ✔ | Before constructor body runs |
Assign in static initializer (static final only) |
✔ | For class-level constants |
| Assign multiple times | ❌ | Compilation error |
| Default initialization | ❌ | Must be explicitly assigned |
Example of an illegal situation:
public class Example {
final int x; // not initialized
}
Example e = new Example(); // ❌ compile-time error
Why final Variables Are Not Default-Initialized?
Because:
- Their value must be known and immutable, and
- Java must guarantee that the value is set before use,
- Default initialization would create a situation where
0,null, orfalsemight unintentionally become the permanent value.
Thus, Java forces developers to explicitly initialize final fields.
Tip
final means assigned once, not immutable object.
A final reference can still point to a mutable object.
final List<String> list = new ArrayList<>();
list.add("ok"); // allowed
list = new ArrayList<>(); // ❌ cannot reassign reference
6.2.3 Local variables
Local variables are variables defined within a constructor, method or inizializer block;
Local variables do not have default values and they must be initialized before they can be used. If you try to use a not initialized local variable the compiler will report an ERROR.
- Example
public int localMethod {
int firstVar = 25;
int secondVar;
secondVar = 35;
int firstSum = firstVar + secondVar; // OK variables are both initialized before use
int thirdVar;
int secondSum = firstSum + thirdVar; // ERROR: variable thirdVar has not been initialized before use; if you do not try to use thirdVar the compiler will not complain
}
6.2.3.1 Inferring Types with var
Under certain conditions you can use the keyword var in place of the appropriate type when declaring local variables;
Warning
- var IS NOT a reserved word in java;
- var can be used only for local variables: it CAN'T be used for constructor parameters, instance variables or method parameters;
- The compiler infere the type by looking ONLY at the code on the line of the declaration; once the right type has been inferred you can't reassign to another type.
- example
public int localMethod {
var inferredInt = 10; // The compiler infer int by the context;
inferredInt = 25; // OK
inferredInt = "abcd"; // ERROR: the compiler has already inferred the type of the variable as int
var notInferred;
notInferred = 30; // ERROR: in order to infer the type, the compiler looks ONLY at the line with declaration
var first, second = 15; // ERROR: var cannot be used to define two variables on the same statement;
var x = null; // ERROR: var cannot be initialized with null but it can be reassigned to null provided that the underlying type is a reference type.
}
Warning
Local variables never get default values. Instance & static fields always do.
6.3 Wrapper Types
In Java, wrapper types are object representations of the eight primitive types.
Each primitive has a corresponding wrapper class in the java.lang package:
| Primitive | Wrapper Class |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
Wrapper objects are immutable — once created, their value cannot change.
6.3.1 Purpose of Wrapper Types
- Allow primitives to be used in contexts that require objects (e.g., collections, generics).
- Provide utility methods for parsing, converting, and working with values.
- Support constants such as
Integer.MAX_VALUEorDouble.MIN_VALUE.
6.3.2 Autoboxing and Unboxing
Since Java 5, the compiler automatically converts between primitives and their wrappers:
- Autoboxing: primitive → wrapper
- Unboxing: wrapper → primitive
Integer i = 10; // autoboxing: int → Integer
int n = i; // unboxing: Integer → int
Integer int1 = Integer.valueOf(11);
long long1 = int1; // Unboxing --> implicit cast OK
Long long2 = 11; // ❌ Does not compile.
// 11 is an int literal → requires autoboxing + widening → illegal
Character char1 = null;
char char2 = char1; // WARNING: NullPointerException
Integer arr1 = {11.5, 13.6} // WARNING: Does not compile!!
Double[] arr2 = {11, 22}; // WARNING: Does not compile!!
Tip
Java never performs autoboxing + widening/narrowing in one step.
Warning
- AUTOBOXING and Implicit cast are not allowed in the same statement: you can't do both at the same time. (see example above)
- This rule apply also in method calls.
6.3.3 Parsing and Conversion
Wrappers provide static methods to convert strings or other types into primitives:
int x = Integer.parseInt("123"); // returns primitive int
Integer y = Integer.valueOf("456"); // returns Integer object
double d = Double.parseDouble("3.14");
// On the numeric wrapper class valueOf() throws a NumberFormatException on invalid input.
// Example:
Integer w = Integer.valueOf("two"); // NumberFormatException
// On Boolean, the method returns Boolean.TRUE for any value that matches "true" ignoring case, otherwise Boolean.false
// Example:
Boolean.valueOf("true"); // true
Boolean.valueOf("TrUe"); // true
Boolean.valueOf("TRUE"); // true
Boolean.valueOf("false"); // false
Boolean.valueOf("FALSE"); // false
Boolean.valueOf("xyz"); // false
Boolean.valueOf(null); // false
// The numeric integral classes Byte, Short, Integer and Long include an overloaded **valueOf(String str, int base)** method which takes a base value
// Example with base 16 (hexadecimal) which includes character 0 -> 9 and A -> F (ignore case)
Integer.valueOf("6", 16); // 6
Integer.valueOf("a", 16); // 10
Integer.valueOf("A", 16); // 10
Integer.valueOf("F", 16); // 15
Integer.valueOf("G", 16); // NumberFormatException
Note
methods parseXxx() return a primitive while valueOf() returns a wrapper object.
6.3.4 Helper methods
All the numeric wrapper classes extend the Number class and, for that, they inherit some helper methods such as: byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue().
The Boolean and Character wrapper classes include: booleanValue() and charValue().
- Example:
// In trying to convert those helper methods can result in a loss of precision.
Double baseDouble = Double.valueOf("300.56");
double wrapDouble = baseDouble.doubleValue();
System.out.println("baseDouble.doubleValue(): " + wrapDouble); // 300.56
byte wrapByte = baseDouble.byteValue();
System.out.println("baseDouble.byteValue(): " + wrapByte); // 44 -> There is no 300 in byte
int wrapInt = baseDouble.intValue();
System.out.println("baseDouble.intValue(): " + wrapInt); // 300 -> The value is truncated
6.3.5 Null Values
Unlike primitives, wrapper types can hold null. Attempting to unbox null causes a NullPointerException:
Integer val = null;
int z = val; // ❌ NullPointerException at runtime
6.4 Equality in Java
Java provides two different mechanisms for checking equality:
==(equality operator).equals()(method defined inObjectand overridden in many classes)
Understanding the difference is essential.
6.4.1 Equality with Primitive Types
For primitive values (int, double, char, boolean, etc.),
the operator == compares their actual numeric or boolean value.
Example:
int a = 5;
int b = 5;
System.out.println(a == b); // true
char c1 = 'A';
char c2 = 65; // same Unicode code point
System.out.println(c1 == c2); // true
6.4.1.1 Key points
==performs value comparison for primitives.- Primitive types have no
.equals()method. - Mixed primitive types follow numeric promotion rules
(e.g.,int == long→intpromoted tolong).
6.4.2 Equality with Reference Types
With objects (reference types), the meaning of == changes.
6.4.2.1 ==(Identity Comparison)
== checks whether two references point to the same object in memory.
String s1 = new String("Hi");
String s2 = new String("Hi");
System.out.println(s1 == s2); // false → different objects
Even if contents are identical, == is false unless both variables refer to
the exact same object.
6.4.2.2 .equals() (Logical Comparison)
Many classes override .equals() to compare values, not memory addresses.
System.out.println(s1.equals(s2)); // true → same content
6.4.2.3 Key points
.equals()is defined inObject.- If a class does not override
.equals(), it behaves like==. - Classes like
String,Integer,List, etc. override.equals()
to provide meaningful value comparison.
6.4.3 String Pool and Equality
String literals are stored in the String pool, so identical literals
refer to the same object.
String a = "Java";
String b = "Java";
System.out.println(a == b); // true → same pooled literal
But using new creates a different object:
String x = new String("Java");
String y = "Java";
System.out.println(x == y); // false → x is not pooled
System.out.println(x.equals(y)); // true
Common Pitfalls
String x = "Java string literal";
String y = " Java string literal".trim();
System.out.println(x == y); // false → x and y are not the same at compile time
String a = "Java string literal";
String b = "Java ";
b += "string literal";
System.out.println(a == b); // false
Warning
Any String created at runtime does not go into the pool automatically.
Use intern() if you want pooling.
Tip
"Hello" == "Hel" + "lo" → true (compile-time constant)
"Hello" == getHello() → false (runtime concatenation)
String x = "Hello";
String y = "Hel" + "lo"; // compile-time → same literal
String z = "Hel";
z += "lo"; // runtime → new String
System.out.println(x == y); // true
System.out.println(x == z); // false
6.4.3.1 The intern method
You can also tell Java to use a String from the String Pool (in case it already exist) through the intern() method:
String x = "Java";
String y = new String("Java").intern();
System.out.println(x == y); // true
6.4.4 Equality with Wrapper Types
Wrapper classes (Integer, Double, Boolean, etc.) behave like normal objects.
Therefore:
==→ compares object references.equals()→ compares numeric values
Example:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true → cached
Integer c = 1000;
Integer d = 1000;
System.out.println(c == d); // false → different objects
System.out.println(c.equals(d)); // true → same numeric value
Since, as previously noted, all wrapper classes are immutable, their internal value cannot be modified once they are created.
Operations that appear to modify a wrapper actually create a new object.
Example:
Integer i = 5;
i++;
This is conceptually equivalent to:
i = Integer.valueOf(i.intValue() + 1);
Therefore a new Integer object is created and assigned back to i.
6.4.4.1 Wrapper Caching
To reduce memory usage and object creation, Java reuses certain wrapper instances.
The following values are cached:
- All
Booleanvalues (trueandfalse) - All
Bytevalues - All
Charactervalues from\u0000to\u007f(0–127) - All
Shortvalues from −128 to 127 - All
Integervalues from −128 to 127
Because of this caching mechanism:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Both variables refer to the same cached object.
However, values outside the cache range produce distinct objects:
Integer c = 1000;
Integer d = 1000;
System.out.println(c == d); // false
System.out.println(c.equals(d)); // true
6.4.4.2 The new keyword bypasses the cache
When a wrapper object is created using new, a new instance is always created, even if a cached value exists.
Example:
Integer i = 10; // cached object
Integer j = 10; // same cached object
Integer k = new Integer(10); // new object (not cached)
System.out.println(i == j); // true
System.out.println(i == k); // false
However, wrapper constructors were deprecated in Java 9 and marked for removal.
Modern code should use autoboxing or factory methods such as Integer.valueOf().
6.4.4.3 Comparing wrapper objects
When two wrapper references are compared using ==, the result depends on whether they refer to the same object, not on whether their values are equal.
Therefore, equality tests between wrappers should normally use:
equals()
instead of ==.
6.4.4.4 Different Wrapper Types Cannot Be Compared
Wrapper objects of different types cannot be compared using ==.
Example:
Byte b = 1;
Integer i = 1;
b == i; // compilation error
The operands must be compatible types, otherwise the comparison is not valid.
Warning
Be very careful when comparing wrapper objects with ==.
Due to wrapper caching, comparisons may sometimes return true and sometimes false depending on the value.
6.4.5 Equality and null
== nullis always safe.- Calling
.equals()on anullreference throws a NullPointerException.
String s = null;
System.out.println(s == null); // true
// s.equals("Hi"); // ❌ NullPointerException
6.4.6 Summary Table
| Comparison | Primitives | Objects / Wrappers | Strings |
|---|---|---|---|
== |
compares value | compares reference | identity (affected by String pool) |
.equals() |
N/A | compares content if overridden | content comparison |