Types
Table of Contents
Introduction
In a previous chapter, I wrote that data is an essential "information abstraction" that falls under the category of "computational necessity". This idea was further developed when we looked at how data is stored in a machine, both as constant values and as variables. Now, we'll explore how different kinds of data are interpreted by the Java programming language and the rules that govern them.
Type System
We now know that all data in a computer is stored as a collection of binary digits (bits). We also know that in Java, if I want to store numeric information I can store it in something called an int variable. I could declare one such variable using the statement int x = 65, which says that x is a symbolic name for a location in memory holding the bits 1000001. But we also know that 1000001 can be interpreted as the letter 'A' in ASCII. What determines how those bits are interpreted?
In programming languages, one way of managing the interpretation of data is through the construction of a type system. In Benjamin Pierce's seminal book, Types and Programming Languages, he provides a possible definition, "a type system is a tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute." This is a highly technical and accurate, albeit a bit bureaucratic, definition of a type system. I can give a bit more of a looser, intuitive definition,
- A type system provides the rules for different kinds of data.
This means that type systems lay out all of the ways that data can interact during a program's execution.
Nearly every high-level programming language in use today1 has a type system. Type systems provide a variety of benefits to programmers:
- Error checking
- Converting a string into a number before taking the square root
- Maintenance
- Update the definition of a data type and its updated everywhere
- Enforces contracts
- Guarantees behaviors of two variables
- Documentation
- Easier to read code when types are involved
- Language safety
- A safe language is one that protects its own abstractions
The Java programming language has a type system and it is important you understand how it works.
Java is Statically Typed
Firstly, Java is statically typed. This means that every single variable and function within a program must have a type assigned to it at compile-time. Here is an example of a small Java program:
1: public class Decl{ 2: public static void main(String[] args){ 3: int i = 10; 4: float f = 10.5f; 5: String s = "Hello, World!"; 6: } 7: }
In order for the above program to compile, variables i, f, and s must all have types associated with them—this is different from dynamically typed languages like Python where the programmer does not need to explicitly specify the type at the time of declaration. i is declared with the type int, f with the type float, and s with the type String. Here's what happens if I remove the int from line 2 in Listing 1:
josephraskind@stargazer:/tmp/types$ javac Decl.java
Decl.java:3: error: cannot find symbol
i = 10;
^
symbol: variable i
location: class Decl
1 error
We can see in the error message from Listing 2 that something went wrong in the compilation process. The error message, however, looks a bit strange. It says that i must cannot be found. Again, this is because all variables in Java must be declared with a type—any reference to a variable without a type identifier behind it immediately tells the compiler that the variable is not being declared, but is being used.
The compiler does not just check to see that every variable/function has a type, but it also checks to make sure that variables/functions follow the rules of their declared types. For example if I alter Listing 2 to:
1: public class Decl{ 2: public static void main(String[] args){ 3: int i = 10; 4: float f = "Hello, World"; 5: String s = "Hello, World!"; 6: } 7: }
You will have noticed that String looks a bit different than int and float, this is because it is a reference type. We will learn more about reference types later on.
When I try to compile the code I get the following message:
josephraskind@stargazer:/tmp/types$ javac Decl.java
Decl.java:4: error: incompatible types: String cannot be converted to float
float f = "Hello, World";
^
1 error
This happens because the type assigned to the expression on the right side of the = is not the same as the type assigned to the variable on the left side (and cannot be implicitly converted).
Java is Strongly Typed
Another example of a statically typed language is C. Although C is statically typed, it's often not considered strongly typed. This is because it is very easy to "break" the type system. For example if I slightly change the sort of alteration performed in Listing 3, to the following:
1: int main(){ 2: int i = "Hello, World!"; 3: float f = 10.5; 4: const char * s = "Hello, World!"; 5: }
Then I compile it:
josephraskind@stargazer:/tmp/types$ gcc types.c
types.c: In function ‘main’:
types.c:2:11: warning: initialization of ‘int’ from ‘char *’ makes integer from pointer without a cast [-Wint-conversion]
2 | int i = "Hello, World!";
| ^~~~~~~~~~~~~~~
I dont get an error?
Technically, the above does not "break" the type system, but it does break reasonable expectations from the programmers perspective. It seems nonsensical to be able to assign an integer to the string constant "Hello, World!". In reality, that's not exactly what's happening…
Java, in contrast, is much better at preventing these sorts of wanton conversions. There are limitations, especially at the frontiers of the language, but generally the advantage that Java provides over other languages is the relative safety associated with its type system. For example, let's take a similar example to Listing 5:
1: public class TypeSafety { 2: public static void main(String[] args) { 3: int i = "Hello, World!"; // Does this work? 4: } 5: }
And when we compile it:
josephraskind@stargazer:/tmp/types$ javac TypeSafety.java
TypeSafety.java:3: error: incompatible types: String cannot be converted to int
int i = "Hello, World!"; // Does this work?
^
1 error
Unlike the C example, this Java code does not compile. Java refuses to allow it to reach the JVM execution stage. This is the practical difference between weak and strong typing: C trusts the programmer and produces a runnable, if "broken", binary, while Java enforces the contract at compile time and stops the process entirely.
Java Types
We will now go through, almost, all of the different types in Java. Starting with the basic types.
Primitive Types
Java makes a distinction between primitive types and reference types. For the time being we will be concerned only with primitive types. Later on we will discuss reference types in greater detail. The basic, primitive types are as categorized as follows (Java Language Specification § 4.2):
- Integral Types
- Floating-Point Types
- Boolean Type
Integral Types
The integral types are defined as follows:
- For
byte, from -128 to 127, inclusive - For
short, from -32768 to 32767, inclusive - For
int, from -2147483648 to 2147483647, inclusive - For
long, from -9223372036854775808 to 9223372036854775807, inclusive - For
char, from '\u0000' to '\uffff' inclusive, that is, from 0 to 65535
Floating-Point Types
The floating-point types are defined as follows:
- For
float, 32-bit binary32 floating-point format for IEEE 754 - For
double, 64-bit binary64 floating-point format for IEEE 754
Boolean Type
The boolean type simply stores the literals true and false.
Type Conversion
Sometimes mixing and matching types is legal in the language despite intuition pointing the opposite direction. This sort of type fluidity is called type conversion. One data type is converted to another based on a set of rules by the language specification. There are two kinds of type conversion: explicit and implicit.
Explicit Conversion
Explicit type conversion occurs when the programmer uses direct syntax to indicate the change from one type to another. Here is an example:
1: public class ExplicitConversion{ 2: public static void main(String[] args){ 3: double d = (double) 10; 4: } 5: }
What we see on Line 3 is an explicit type conversion from the constant 10's int type to a double. Placing the name of the desired type in parentheses prior to the expression explicitly tells the compiler to convert the int to a double—this is called a "cast". In Line 3 we cast the int 10 to the double 10.0.
Implicit Conversion
Implicit type conversion occurs in situations where a type issue arises, but can be resolved with the compiler's intervention between compatible types. An example:
1: public class ImplicitConversion{ 2: public static void main(String[] args){ 3: double d = 10; 4: } 5: }
Listing 10 works exactly the same as Listing 9 even though there is no explicit cast. The compiler recognizes that there is a type compatibility between doubles and ints as they are both numeric types (arithmetic types in the spec).
Conversion Rules
As we saw above, the Java programming language allows for conversions, both implicit and explicit, between data types. In order for this to be deterministic the language specification details exactly how certain conversions are meant to be achieved/implemented by the compiler. You are not responsible for memorizing all of the edge cases provided by the language specification for handling type conversion, but you should be aware that it can happen. Some rules you should be aware of:
- Java does not allow "narrowing" conversions to occur implicitly
- Going from an int to a double is allowed, but a double to an int must be explicitly cast
- When converting from real values to integer values the fractional portion is always discarded
- When a larger data type is converted to a smaller data type (i.e., int to char), the least significant bits are retained.
- "Narrower" types will be automatically converted to "wider" types
float+int→float+float
Type Modifiers
Java also provides the ability for programmers to assign "extra" rules to a variable by providing type modifiers. As of Java23 there are three type modifiers: final, volatile, and transient. In this course, we will only concern ourselves with final. The final modifier turns the variable into a "constant", meaning once it has been assigned a value, that value cannot be changed. For example:
1: public class Final{ 2: public static void main(String[] args){ 3: final int i; 4: i = 10; // No problem 5: i = 20; // Problem! 6: } 7: }
The above code will not compile because the integer i cannot be assigned a second value. You may think that it's odd that Java provides the ability to write constant values. After all, why not just write 10 for every instance of i? The reason is that sometimes you want to change constants when testing your code. For example, if I'm testing to see how many students pass CS210 it's easier for me to change a constant variable final double PASS_SCORE than it is for me to individually change it throughout my entire source code file. This is doubly true if the constant is propagated throughout dozens of files.
Exercises
- The chapter states that narrower types are automatically converted to wider types in mixed arithmetic expressions. Write a Java program that divides two
intvariables where the result has a fractional component (e.g.7 / 3), stores it in adouble, and prints it. Does the fractional part appear? Now try storing the result of(double) 7 / 3in adoubleand print again. Explain the difference in terms of implicit and explicit conversion. - Write a Java program that declares a
final intrepresenting the passing grade for this course (60). Attempt to change it after declaration and record the compiler error. Then remove thefinalqualifier and confirm it compiles. Why might usingfinalbe preferable to just writing the number 60 throughout your code? - Write a Java program that explicitly casts each of the following and prints the result:
(int) 3.9,(int) -3.9,(char) 65,(double) 7 / 2,(double) (7 / 2). Before running, predict each result. Explain any surprises. - The chapter states that when converting from a larger type to a smaller type, the least significant bits are retained. Write a Java program that assigns
int i = 256to abyte bvia explicit cast and prints both. Explain the result in terms of bit representation. - Java provides a way to inspect the size of primitive types through wrapper classes. Print the size in bits of
Byte,Integer,Float, andDoubleusingByte.SIZE,Integer.SIZE,Float.SIZE, andDouble.SIZE. Do they match what the chapter states? - The chapter contrasts Java's strong typing with C's weak typing using Listing 8. Write a Java program that attempts three different invalid type assignments of your own invention and record the compiler errors for each. For each error, explain in your own words why Java refuses to allow it.
- Write a Java program that demonstrates implicit conversion in an arithmetic expression: declare an
intand adouble, add them together, and store the result in both anintand adoublevariable. Does theintstorage require an explicit cast? Print all variables and explain what you observe. - The chapter mentions that Java is considered strongly typed while C is considered weakly typed despite both being statically typed. In your own words, explain the difference between static typing and strong typing using examples from the chapter.
- Using Java's wrapper classes, print
Integer.MAX_VALUE,Integer.MIN_VALUE,Byte.MAX_VALUE, andByte.MIN_VALUE. Verify that the ranges match what you would expect given the bit sizes from the previous exercise. - Write a Java program that stores the value
0xFFFFFFFFcast to anintand performs the following operations: addition of 1, division by 2, and multiplication by 2. Print all results. Then repeat using alonginstead of anint. For each operation, explain why theintandlongresults differ.