Variables are devices that are used to store data, such as a number, or a string of character data so that we can manipulate them later in our program.
Variables can be broadly classified in to 4 types in Java:
- Class variables (static, declared in a class).
- Instance variables (non-static, declared in a class).
- Local variables (declared inside a method).
- Block variables (variables in static blocks, for-loop blocks etc).
Instance variables and objects reside in heap whereas local variables reside in stack. Consider the below program:
class Collar {
}
class Dog {
Collar c; // instance variable
String name; // instance variable
public static void main(String[] args) {
Dog d; // local variable: d
d = new Dog();
d.go(d);
}
void go(Dog dog) { // local variable: dog
c = new Collar();
dog.setName("Aiko");
}
void setName(String dogName) { // local var: dogName
name = dogName;
// do more stuff
}
}
For the above program, the instance variables, objects and local variables will be stored in memory as shown in the figure below:
Literal Values for All Primitive Types
Literals are nothing but values that a particular data type can hold. A primitive literal is merely a source code representation of the primitive data types, in other words, an integer, floating-point number, boolean, or character etc. that you type in while writing code. The following are examples of primitive literals:
Integer Literals
There are four ways to represent integer numbers in the Java language: decimal (base 10), octal (base 8), hexadecimal (base 16), and from Java 7, binary (base 2).
One more new feature introduced in Java 7 was numeric literals with underscores (_) characters. This was introduced to increase readability. See below:
But you must keep in mind the below gotchas:
NOTE: You can use the underscore character for any of the numeric types (including doubles and floats), but for doubles and floats, you CANNOT add an underscore character directly next to the decimal point.
Decimal Literals
These are numbers with a radix of 10 which we use most commonly. They do not need prefix of any kind and are initialized as below:
Binary Literals
From Java 7, you can initialize variables holding binary literals. But they must start with either 0B
or 0b
, as shown
below:
Octal Literals
Octal integers use only the digits 0 to 7. They have a radix of 8. In Java, you represent an integer in octal form by placing a zero in front of the number, as follows:
You can have up to 21 digits in an octal number, not including the leading zero. This is because no mater what number
system you use, the range of values that an int
can hold is always between \(-2^{31}\) to \(+2^{31}-1\).
Hexadecimal Literals
Hexadecimal (hex for short) numbers are constructed using 16 distinct symbols. They have a radix of 16. Counting from 0 through 15 in hex looks like this:
Java accepts uppercase or lowercase letters for the extra digits (one of the few places Java is not case-sensitive).
You represent an integer in hexadecimal form by placing a 0x
in front of the number, as follows:
All four integer literals (binary, octal, decimal, and hexadecimal) are defined as int
by default, but they may also
be specified as long
by placing a suffix of L
or l
after the number:
Floating-point Literals
Floating-point numbers are defined as a number, a decimal symbol, and more numbers representing the fraction. For example,
By default, floating-point literals are defined as double
(64 bits) so if you want to assign a floating-point literal
to a variable of type float
(32 bits), you must attach the suffix F
or f
to the number. So, the below code
generates a compiler error:
This happens because we’re trying to fit a larger number (64 bits) into a (potentially) less precise “container” (32 bits).
Now as by default floating-point literals are of type double
, it is optional to attach a suffix of D
or d
when you
want to assign it to a variable of type double
. For example,
double d = 110599.995011D; // Optional, not required
double g = 987.897; // No 'D' suffix, but OK because the
// literal is a double by default
Boolean Literals
Boolean literals can be either true
or false
. In C (and some other languages) it is common to use numbers to
represent true or false, but this will not work in Java. For example,
boolean t = true; // Legal
boolean f = 0; // Compiler error!
int x = 1; if (x) { } // Compiler error!
Character Literals
A char literal is represented by a single character in single quotes:
char a = 'a';
char b = '@';
You can also assign unicode value to a char
variable, like:
char letterN = '\\u004E'; // The letter 'N'
Note, characters are nothing but 16-bit unsigned integers. So, you can assign a number literal, assuming it will fit
into the unsigned 16-bit range (0 to 65535) to a char
variable. For example, the following are all legal:
char a = 0x892; // hexadecimal literal
char b = 982; // int literal
char c = (char)70000; // The cast is required; 70000 is
// out of char range
char d = (char) -98; // Ridiculous, but legal
And the following are not legal and produce compiler errors:
char e = -29; // Possible loss of precision; needs a cast
char f = 70000; // Possible loss of precision; needs a cast
Literal values for Strings
You can create a String
in Java in the following ways:
String s = "tutorial";
String str = new String("Rahul roy");
String con = s + str; // concatenate 2 strings
Strings are not primitives in Java but can be represented as literals, in other words, they can be typed directly into code like:
System.out.println("Bill" + " Joy");
Literal values for Non-Primitives
Variables are just bit holders, with a designated type. You can have an int
holder, a double
holder, a long
holder,
and even a String[]
holder. This holder is assigned a bunch of bits representing the value. For primitives, the bits
represent a numeric value but for non-primitives, these bits represent a way to get to the object.
For example, a byte
with a value of 6
means that the bit pattern in the variable (the byte
holder) is 00000110
,
representing the 8 bits. But what happens in case of non-primitives, for example, Button b = new Button();
, what’s
inside the Button
holder b
? Is it the Button object? No! A variable referring to an object is just a reference
variable. A reference variable bit holder contains bits representing a way to get to the object. We don’t know
what the format is. The way in which object references are stored is virtual-machine specific (it’s a pointer to
something, we just don’t know what that something really is). All we can say for sure is that the variable’s value is
not the object, but rather a value representing a specific object on the heap. Or null
. When it is null
, i.e,
Button b = null;
you can say that the reference variable b
is not referring to any object.
There is one important concept to understand here, i.e, a reference variable can refer to any object that is a subclass of the declared reference variable type but not a superclass. Let’s see why.
class Foo {
public void doFooStuff() { }
}
class Bar extends Foo {
public void doBarStuff() { }
}
class Test {
public static void main (String \[\] args) {
Foo reallyABar = new Bar(); // Legal because Bar is a
// subclass of Foo
Bar reallyAFoo = new Foo(); // Compiler error! Foo is not a
// subclass of Bar
}
}
In line 11, reallyAFoo
is a Bar
reference variable (child) so someone would call reallyAFoo.doBarStuff()
but the
reference variable actually holds a Foo
object (parent) which doesn’t have a doBarStuff()
method. So, the compiler
prevents this and gives a Incompatible types
error.
In other words, a child class is nothing but the parent class with additional properties. So there is no issue in line 9,
where a Foo
reference variable (parent) is holding a Bar
object (child). Because everything a Foo
object can do,
can also be done by a Bar
object.
Casting
Casting is a way of converting literal values/objects from one type to another. When the type of variable is different from the type of literal/object it’s holding/referring, you may require casting.
Casting can be done by the compiler (implicit cast) or by you (explicit cast). Typically, an implicit cast happens when you’re doing a widening conversion, in other words, putting a smaller thing (say, a byte) into a bigger container (such as an int). But when you try to put a large value into a small container (referred to as narrowing), you should do an explicit cast, where you tell the compiler that you’re aware of the danger and accept full responsibility. Let’s for example consider the below program:
class Casting {
public static void main(String \[\] args) {
long a = 100; // literal '100' is implicitly an 'int'
//but the compiler does an implicit cast
int b = (int) 10.23; // literal '10.23' is implicitly a 'double'
// so we require an explicit cast
int x = 3957.229; // illegal, can't store a large value in a
// small container without explicit cast
}
}
There are some rules which you must be aware of:
- The result of an expression involving anything int-sized or smaller is always an
int
, so we must explicitly cast it. Check this out:
The last line won’t compile! You’ll get an error like this:
Doing an explicit cast like:
solves the issue.
- In case of compound assignment operators, explicit cast isn’t required. The below code compiles just fine:
What happens when you cast a large value to store it in a small container
When you do a explicit cast, the compiler just keeps the number of bits (from right) that the variable type can hold and strips off the rest. Consider the below program to understand better:
class Casting {
public static void main(String \[\] args) {
int i = 7;
byte b = (byte) i;
System.out.println("The 1st byte is " + b); // prints 7
// now let's see another example
int i = 128;
byte b = (byte) i;
System.out.println("The 2nd byte is " + b); // prints -128
}
}
So, in line 4, i
is 7
i.e, 00000000000000000000000000000111
(32 bits) and when we do a explicit cast, the compiler just stores 00000111
(8 bits) in variable b
(as byte
can hold only 8 bits) which is also 7
. Therefore, it prints 7
. But in line 8, i
is 128
i.e, 00000000000000000000000010000000
and after stripping off the extra bits we are left with 10000000
which is not 128
as the 1st bit is the sign bit. So, after computing the 2’s compliment of it we get -128
as the result.
Scope
Scope refers to the lifetime and accessibility of a variable. In simple words, how long will the variable be hanging around so that they can be used by other parts of the program. Different types of variables have different scope.
- Class or static variables have the longest scope. They are created when the class is loaded, and they survive as long as the class stays loaded in the Java Virtual Machine (JVM).
- Instance or non-static variables are the next most long-lived. They are created when a new instance is created, and they live until the instance is removed.
- Local variables are next. They live as long as their method remains on the stack. As we’ll soon see, however, local variables can be alive and still be “out of scope”.
- Block variables live only as long as the code block is executing.
Below program shows all types of variables and explains their scopes too. Please refer to the comments to understand which are in scope and which are out of scope.
class Scope {
static int s = 343; // static variable
int x; // instance variable
{ // initialization block
x = 7;
int x2 = 5; // block variable
}
Scope() { // constructor
x += 8;
int x3 = 6;
}
void doStuff() { // method
int y = 0; // local variable
for (int z = 0; z < 4; z++) { // 'for' code block
y += z + x;
}
z++; // compiler error (out of scope)
x2++; // compiler error (out of scope)
}
public static void main(String[] a) {
x++; // compiler error! 'x' is instance variable, so
// we need an object to access it
x2++; x3++; // compiler error! block variables scope
// is only inside the block in which they
// are declared
}
}
Variable Initialization
Java gives us the option of initializing a declared variable or leaving it uninitialized. When we attempt to use the uninitialized variable, we can get different behavior depending on what type of variable or array we are dealing with (primitives or objects). The behavior also depends on the level (scope) at which we are declaring our variable.
Default values for Instance variables (Primitive and Non-primitive):
Variable Type | Default Value |
---|---|
byte, short, int, long | 0 |
float, double | 0.0 |
boolean | false |
char | ’\u0000’ |
Object reference | null (not referencing any object) |
Therefore, for the below program:
public class Book {
private String title; // instance reference variable
private int noOfPages; // instance primitive variable
public String getTitle() {
return title;
}
public int getNoOfPages() {
return noOfPages;
}
public static void main(String \[\] args) {
Book b = new Book();
System.out.println("The title is " + b.getTitle());
System.out.println("No. of pages are " + b.getNoOfPages());
}
}
The output will be:
The title is null
No. of pages are 0
NOTE: null
is not the same as an empty String ("")
. A null
value means the reference variable is not referring
to any object on the heap.
Array Instance Variable
An array is an object, thus, an array instance variable that’s declared but not explicitly initialized will have a value
of null
, just as any other object reference instance variable. But if the array is initialized, all array elements
are given their default values, the same default values that elements of that type get when they’re instance variables.
In short, Array elements are always, always, always given default values, regardless of where the array itself
is declared or instantiated.
Variable Type | Default Value |
---|---|
Array (uninitialized) | null |
Array (initialized) | Default values of their respective types as discussed above |
Default values for Local (also called Stack or Automatic) variables (Primitive and Non-primitive):
Local variables, including primitives, always, always, always must be initialized before you attempt to use them (though not necessarily on the same line of code). Java does not give local variables a default value, you must explicitly initialize them with a value.
Q&A
Q1. Given the below program:
public class Fishing {
byte b1 = 4;
int i1 = 123456;
long L1 = (long)i1; // line A
short s2 = (short)i1; // line B
byte b2 = (byte)i1; // line C
int i2 = (int)123.456; // line D
byte b3 = b1 + 7; // line E
}
Which lines WILL NOT compile? (Choose all that apply.)
A. Line A
B. Line B
C. Line C
D. Line D
E. Line E
Q2. Given the below program:
class Mixer {
Mixer() {
}
Mixer(Mixer m) {
m1 = m;
}
Mixer m1;
public static void main(String[] args) {
Mixer m2 = new Mixer();
Mixer m3 = new Mixer(m2);
m3.go();
Mixer m4 = m3.m1;
m4.go();
Mixer m5 = m2.m1;
m5.go();
}
void go() {
System.out.print("hi ");
}
}
What is the result?
A. hi
B. hi hi
C. hi hi hi
D. Compilation fails
E. hi, followed by an exception
F. hi hi, followed by an exception
Q3. Given the below program:
class Fizz {
int x = 5;
public static void main(String[] args) {
final Fizz f1 = new Fizz();
Fizz f2 = new Fizz();
Fizz f3 = FizzSwitch(f1, f2);
System.out.println((f1 == f3) + " " + (f1.x == f3.x));
}
static Fizz FizzSwitch(Fizz x, Fizz y) {
final Fizz z = x;
z.x = 6;
return z;
}
}
What is the result?
A. true true
B. false true
C. true false
D. false false
E. Compilation fails
F. An exception is thrown at runtime