Welcome back to another Java tutorial! If you are following along, then you just finished playing around with numbers in DrJava’s interactions pane. In this tutorial, we will be giving some context to some of the examples from the previous tutorial. In particular, we’ll be covering Java primitive types and their role in variable creation. Let’s get started!
Table of Contents
Before we really dig into the code, we need to talk about primitive types and their capabilities.
Java Primitive Types
In every programming language, there are data types that are built-in to the language. In languages like Java where all data types are explicit, each one has a unique keyword. These explicit keywords are used to tell the language what types we want to use.
By now we should have had some exposure to the following types:
- int: a 32-bit signed integer
- double: a 64-bit signed floating-point number
- char: a 16-bit unsigned integer
In addition to these types, there are five others: byte, short, long, float, and boolean. We will likely spend most of our time in Java working with the three types we have discussed already as well as the boolean type.
For a comprehensive look at all of the primitive types, Oracle provides a complete list in their Java documentation. However, let’s run over these quickly!
The Int Primitive Type
As we’ve explored already, Java integers are 32-bit signed values, and they are denoted by the
int someNumber = 10;
Of course, like all primitive types, integers have their limits. Since they’re 32-bit, we’re looking at a range of values from -2,147,483,648 to 2,147,483,647. That’s a lot of numbers! Of course, we can confirm that range using a handy trick in DrJava’s interactions pane:
Integer.MAX_VALUE // Prints 2,147,483,647 Integer.MIN_VALUE // Prints -2,147,483,648
int is probably the most common integer type used for simple calculations. If you need more range, see
The Double Primitive Type
int, Java doubles are 64-bit floating point values, and they are marked by the
double someNumber = 110.55;
As a reminder, floating point numbers are just real numbers. In other words, we gain access to decimal points when we use doubles.
Since doubles are 64-bit, we have access to significantly more numbers than with integers. If we use the same interactions pane trick, we can find out just how massive the range of potential values is:
Double.MAX_VALUE // Prints 1.7976931348623157E308 Double.MIN_VALUE // Prints 4.9E-324
Keep in mind that negative exponents imply extremely small numbers—as opposed to extremely negative numbers—so we’re not exactly looking at the same kind of range as with integers.
double is the default choice for floating point values in Java. The alternative is
The Char Primitive Type
As we’ve seen already, Java chars represent 16-bit characters, and they are marked by the
char someCharacter = 'f';
All characters in Java are defined using single quotes. Meanwhile, double quotes are used to define strings—something we’ll chat about later.
As usual, we can find out the character range using the following code snippet:
Character.MAX_VALUE // Prints '' Character.MIN_VALUE // Prints ''
To make sense of this range, we can always cast the results to an integer (more on that later):
(int) Character.MAX_VALUE // Prints 65535 (int) Character.MIN_VALUE // Prints 0
As it turns out, the
char type is the only unsigned type in Java. In other words, a character can range from 0 to 65,535 where each number maps to a specific character. In order to create characters beyond this list, Java may combine pairs of characters. Check out Reverse a String in Java for some examples.
The Byte Primitive Type
When we were chatting about binary, we talked about the concept of a bit. Well, a group of eight bits makes a byte which Java supports as a primitive type. The
byte type is essentially just an 8-bit number which ranges from -128 to 127. As expected, bytes are denoted by the
byte someByte = 20;
As always, we can confirm the range of a the
byte type using the following code snippet:
Byte.MAX_VALUE // Prints 127 Byte.MIN_VALUE // Prints -128
In my experience, the
byte type is useful for reading and processing raw data. In general, however, we will likely have no use for it since the range of potential values is so small.
The Short Primitive Type
short type is another integer type, but it takes up less space than the int type. In fact, it takes up exactly half the space at 16 bits, and it is marked by the
short someNumber = 11;
In terms of range, the
short type clocks in at just half of the bits as an integer, but we can confirm that as usual:
Short.MAX_VALUE // Prints 32767 Short.MIN_VALUE // Prints -32768
For practical purposes, a
short only has 65,546 possible values. Both
short are typically used when memory and disk space is low. Otherwise, it is safe to use
int as it is the default type when declaring integers.
The Long Primitive Type
On the other end of the spectrum is the
long primitive type. This type represents extremely large integers where we might want values even greater than the
int type can provide. The
long type is a 64-bit signed integer which means the values range into the quintillions.
Naturally, we denote longs with the
long someBigNumber = 1013401346173L;
To demonstrate just how wide of a range a 64-bit value can have, let’s take a look at the following code snippet:
Long.MAX_VALUE // Prints 9,223,372,036,854,775,807 Long.MIN_VALUE // Prints -9,223,372,036,854,775,808
long would be useful for computating the distance light travels in a given time frame. After one second, light travels roughly 300 million meters. If we wrote a program to track the distance of light in real time, the int type would cap out after around 7 seconds while a long could calculate out to around 975 years. Don’t believe me? Check out this Gist which runs through the entire calculation.
The Float Primitive Type
While we often use the
double which is a 64-bit floating point number type, Java supports another floating point number type called the
int, however, Java defaults to
double for floating point values. At any rate, we can denote a 32-bit floating point number using the
float someNumber = 11.4f;
To get an idea of the range of a
float, let’s use our trick again:
Float.MAX_VALUE // Prints 3.4028235E38 Float.MIN_VALUE // Prints 1.4E-45
As we can see, 32 bits really reduces our range and precision. If we want to use a decimal value with less precision than
double while using half the space, the
float type is an option.
The Boolean Primitive Type
Finally, we can cover the
boolean type. To declare a
boolean, we can use the
boolean isBool = true;
Booleans are a bit unique because they don’t represent numeric values like all the other primitive types. In fact, our little
MIN_VALUE trick won’t work here. Instead, booleans represent
false which we might recall from the previous lesson on logic.
We won’t spend much time on this now because it will become part of almost everything we do in Java. That said, we often won’t declare them explicitly like this. Instead, they are the result of comparisons which drive logic in code. Check out the boolean operators section below for more information.
Java Arithmetic Operators
Up to this point, we have introduced arithmetic operators at random as we build up our Java knowledge. However, we have not explained all the operators we have mentioned. In this section, we’ll take a look at a list of the most common Java operators.
The Addition Operator
As we can probably imagine, the plus sign,
+, is the addition operator:
int sum = 11 + 7; // Stores 18
In this example, we add eleven to seven which stores eighteen.
The Subtraction Operator
Like addition, subtraction uses the same minus sign operator,
-, as traditional math:
int difference = 5 - 4; // Stores 1
In this example, the difference between five and four is one.
The Multiplication Operator
As we move away from addition and subtraction, the arithmetic operators become slightly less obvious. In the case of multiplication, we use the asterisk,
int product = 2 * 3; // Stores 6
When we multiple two by three, we get a result of six.
The Division Operator
In addition to multiplication, Java supports the division operators or forward slash,
int quotient = 10 / 2; // Stores 5
If we divide ten by two, we get a result of five.
The Remainder Operator
If we recall, we tried five different operators for arithmetic. Four of these symbols should have been relatively easy to recognize, but one of them is likely new to us:
%. This is the remainder operator and it returns the remainder of a division operation:
int remainder = 5 % 2; // Stores 1
In this example, the result is one because five divided by two is two with a remainder of one. This operator is often used to perform clever functions such as printing the last digit in an integer.
Java Relational Operators
In addition to arithmetic operators, Java supports relational or equality operators. We have already been exposed to one of these operators:
==. In addition, we can compare values using
>=. Try some of the following examples:
6 > 7 110 >= 54 95 < 96 63 <= 100
As we have probably noticed, the result of each of these operations is a
false. In the future, we will see these operators being used to drive all sorts of logic.
Primitive Type Arithmetic
In the previous tutorial, we took a look at a few examples which we tried in the interactions pane of DrJava. If you have not yet tried them, now is your chance. Otherwise, we are going to discuss these examples more deeply now.
Recall that in the list of examples each operation was shown twice. For example:
4 + 5 4 + 5.0
In this pair, the first computation adds two integers together and the second computation adds an integer and a floating point number together. When we complete these computations, we’ll find out that
4 + 5 yields 9 and
4 + 5.0 yields 9.0.
Unfortunately, these examples do not provide enough information to draw any serious conclusions about the results. To continue our exploration, we should attempt some subtraction and multiplication. Again, the results are similar.
It is not until we play with division that things get interesting. For instance:
1 / 2 1.0 / 2
The first example will probably give us an alarming result. Yes, you entered it correctly. In Java,
1 / 2 is 0. However, if we enter
1.0 / 2, we’ll get the result we are expecting (0.5). This should start to give us an idea of what is happening.
When we add two integers together, we would expect the result to be an integer. This carries into all of the operations including division where something like
1 / 2 can result in a value of 0. In computer science, we call this truncation.
Truncation occurs because 32-bit integers can only hold discrete values. Instead of rounding the output, integers just drop any bits that do not fit in the 32-bit window. This is true for all of the data types, but it is often easier to notice with integers.
While truncation can be confusing and counter-intuitive, it comes in handy in some instances such as mapping—we will definitely be exposed to this later.
At any rate, as long as our types are consistent, arithmetic is pretty simple. However, if we are forced to mix compatible types such as integer and double, Java converts the entire result to the widest type. In other words, the type that has the most bits will be the result of the computation.
Perhaps the last topic to touch on for primitive types is this notion of type casting. We already talked about type widening where a computation gets stored in the widest type. Type casting is just the opposite.
Let’s say we had a computation that would result in a double, but we did not care about the decimal result. We can cut the precision using a typecast to integer. This is used all over the place in code, but a good example would be an implementation of rounding. Without any knowledge of control flow, we can implement rounding:
int round = (int) (7.6 + 0.5);
In this example, the number we are trying to round to the nearest whole number is 7.6. If the decimal is less than .5, we want the result to round down. Likewise, if the decimal is .5 or greater, we want the result to round-up.
By adding .5, we force 7.6 to become 8.1. The typecast then truncates the decimal point which results in our properly rounded integer. If the number was 7.4, the computation would force 7.4 to 7.9. Then the typecast would truncate the decimal.
With that in mind, we have covered just about everything we might need to know about the Java primitive types.
Now that we’ve done some reading, let’s dig into the code.
Usage of Primitive Types
In Java, every variable has to have a type. If we want to work with variables, we should get used to the syntax:
int length = 7;
Here, a variable named
length is assigned the value 7. In front of the variable name, we’ll notice the word
int. This is one of Java’s primitive types and it denotes a 32-bit signed integer. If we recall our discussion about bits, we’ll then be able to quickly compute the range of values for Java integers (232 or 4,294,967,296 possible values).
Limits of Primitive Types
As mentioned several times already, we can determine the maximum value of an integer using the following code snippet:
The return value might seem confusing at first, but we will quickly realize that the value is half of the possible range. That must mean the other half of the range is composed of negative values. Try using the following as confirmation:
For fun, let’s see what happens when we push beyond these limits:
Integer.MAX_VALUE + 1 // Prints -2147483648 Integer.MIN_VALUE - 1 // Prints 2147483647
Isn’t that odd? We’ve just observed integer wraparound for the first time. In other words, once we’ve hit the limit of a primitive type, we’ll wraparound to the other side. Keep that in mind as we move forward.
In case it wasn’t already clear, a data type which has its range split between negative and positive values is called a signed type. Likewise, a data type which has an entirely positive range is called an unsigned typed. In either case, the language interprets the bits representing a value.
If the type is signed, the most significant bit is treated as a sign bit. A sign bit of 0 usually denotes positive values while a sign bit of 1 usually denotes negative values. This is important because almost all of Java’s primitive types are strictly signed. A language like C/C++ has support for both signed and unsigned types.
In the interactions pane, try the following:
char letter = 'b';
Earlier, we wrote a similar line where we assigned a variable a value of 7. In that case we were working with integers. In this case, we are working with the
char primitive type which can store character values. With this line, we have now stored our own value in a variable called letter. Go ahead and experiment with the various data types. For instance, we might try any of the following:
boolean hasMoney = true; int hour = 7; double height = 13.7; float gravity = 9.81f; long sixBillion = 6000000000L;
Now that we have some variables declared, try adding some of these values together and note the results. For instance:
hour + height;
The variable names do not make a lot of sense, but this is perfectly legal and will result in 20.7. However, if we try something like:
hasMoney + hour;
We will end up with an error. That is because we’re trying to add a boolean to an integer. Meanwhile, the following is completely legal in Java:
char gravity = 'g'; char speedOfLight = 'c'; gravity + speedOfLight;
We can indeed add these characters together which yields 202 or ‘Ê’. Because the char type is actually a numeric value, we can sum them like integers.
Adding characters is particularly handy if we want to compare characters for ordering. For instance, two letters can be compared alphabetically by comparing their numeric values. A complete list of all the available ASCII characters and there values can be found here.
As an added note, Java characters are 16-bit which gives them much greater variety than the 256 ASCII characters. In addition, the char primitive type is the only Java primitive type that is unsigned.
But What About Strings?
Since we’re on the topic of characters, let’s talk strings. Java has native support for strings which are sequences of characters. However, strings are not a Java primitive type. They are instead a reference type.
A reference type is a bit different from a primitive type. With primitive types, we are free to copy and compare data as needed. This makes development extremely intuitive because of this concept called value semantics. Value semantics imply that variables are immutable, so we don’t have to worry about a copy corrupting the original value.
To test this concept, try the following in DrJava’s interactions pane:
int x = 5; int y = 5; y == x;
Notice that this comparison returns true as expected. Intuitively, 5 equals 5. Now try the following:
String firstName = "Leroy"; String lastName = "Leroy"; firstName == lastName;
In this example, we define two components of someone’s name: Leroy Leroy. Intuitively, we would think comparing both names would return true. After all, both names have the same letters and both names are case-sensitive. However, we get a shocking result of
As it turns out, the
== operator does not compare the strings as expected. The reason for the bad comparison will be explained in more detail in the following tutorial, so for now try comparing more strings. For instance, we could try creating two strings and assigning them to each other:
String firstName = "Leroy"; String lastName = "Jenkins"; firstName = lastName; firstName == lastName;
In this case, the comparison using
== results in
true. Of course, the value comparison is the same, so why would it return
true this time? In the next tutorial, we will take a deeper look at strings and what is really happening when we make a comparison using
Be careful. If you are using an IDE or a web client compiler, you might actually find that the first string example returns true. Doing this taps into some of the optimization techniques employed by the Java Virtual Machine which pools strings for use in an application. In the next tutorial, we will dive into strings a bit deeper and discuss why this is happening, and what it means moving forward.