Lecture 4

Primitive Types

So far we have manipulated different objects and we have built our own classes, but we have overlooked the primitive types which are the basic building blocks for classes in Java. Although we have used the type 'int' for integers and the type 'String' for strings, we did not say very much about them.

Java has eight primitive types:

The first four types are all about integers of different size. They have different upper and lower bounds for the possible values and they take different amout of memory. In principle it is always a good idea to use 'int' except when you need to optimize the memory allocation. The type 'long' is also useful if you have to deal with very large numbers, but this is rarely the case.

The types 'float' and 'double' are for calculations with real numbers, or as they are usually called in computer science - floating point numbers. The difference between 'float' and 'double' is in the number of digits after the floating point that they can represent, and the minimal and maximal value for each type. The type 'float' takes less space but also provides less precision, while 'double' takes twice as much space and double precision.

The type 'boolean' is used for flags with logical values.

The type 'char' is for single characters.

Integer Arithmetics

The most common and the least surpraising operations on integers are addition, substraction and multiplication which are denoted with the usual operators '+', '-' and '*'. The dual operation of multiplication, i.e. division is more complicated with integers since it is not always the case that an integer divided by another integer is still an integer. There are two operators related to division - '/' and '%'. The first computes the integer part of the division and the second the remainder, for example:

4 / 2 -> 2
5 / 2 -> 2
6 / 3 -> 2

4 % 2 -> 0
5 % 2 -> 1
6 % 2 -> 0

The common relation is that for any two integers m and n, it holds that:

m == (m / n) * n + (m % n)

In addition there is the negation operation which is expressed with the usual unary '-', i.e. the negation of the value x is -x.

This basic operations can be used in any expression, for example on the right hand side of an assignment. There are also two shorthands for commonly used kinds of assignments. For example we have used an assignment like this:

count = count + 1

in order to generate sequential numbers. Since this pattern is used very often, there is a short hand for it with all operators:
x = x + nx += n
x = x - nx -= n
x = x * nx *= n
x = x / nx /= n
x = x % nx %= n

The operations 'x = x + 1' and 'x = x - 1' are even more common, so they are further more abreviated to:
x = x + 1x++++x
x = x - 1x----x

We said above that each of the types 'byte', 'short', 'int' and 'long' have different upper and lower bounds. What happens when we exceed this bounds? With the integers this will lead to an effect called overflow. Try this example:

byte b = 127;
System.out.println(b);
b++;
System.out.println(b);

Here 'b' is an 8-bit integer and we set it to the maximal value 127. An attempt to increase it further leads to overflow and the new value is actually the smallest possible value. In this way, continuous increment will lead to cyclic enumeration of all possible integer values.

Since the overflows are dangerous, it is always advisable to use 'int' instead of the smaller integer types. The smaller types are used only in special cases, for instance when we want to save memory and when we are absolutely sure that we are going to deal with small numbers. Even if you use 'int', you can still discover situations where your code leads to overflows. The easiest solution is to change from 'int' to 'long', but if even this is not satisfactory, then Java provides the type 'BigInteger'. Contrary to the normal integer types, 'BigInteger' is not a primitive type but a class and it can contain an arbitrary large number. The only limitation is the amount of memory that you have on your computer. Such big numbers are needed only in rare cases such as in cryptographic algorithms.

If you need to get the minimal and the maximal value for an integer type, then you can use the constants Integer.MIN_VALUE and Integer.MAX_VALUE for 'int', or the corresponding constants from classes Byte, Short and Long.

Floating Point Arithmetics

The first thing to note about the floating point numbers, is the syntax for numbers. In addition to the well known format with the decimal point, i.e. 2.7, 3.14, etc, there is also the so called exponential notation. It is usually used for writing very small or very large numbers. If you want to write for instance 6.62606957 . 10-34 then in Java you write it as 6.62606957e-34.

The operators for floating point arithmetics are almost the same as for integers. The only difference is that the floating point division is exact, i.e. what you would expect from the school classes in math.

The overflow however works differently since in addition to the normal values the floating numbers has two more values +Infinity and -Infinity. For example this code:

double x = 1e300;
System.out.println(x*1e10);

prints 'Infinity' instead of the expected 1e310. Infinity is also the result if we try to divide something by zero. This is different from the integers where the result would be a runtime error.

In addition to the standard arithmetic operations, Java also provides all familiar mathematical functions such as 'sin', 'cos', 'exp', etc, and constants like PI and E. The surprising thing for them is that the usual matematical notation is not supported and instead we have to call static methods in the class Math from the package java.lang. For example the cosine of π can be computed as:

double x = Math.cos(Math.PI);

Just like with the integers, Java also provides the class BigDecimal which can represent arbitrary real numbers. This class is recommended when it is necessary to compute the exact result and no rounding is permited. The type BigDecimal is also the solution for the problem of precision loss. The floating point types can represent only some real numbers. When the numbers grow bigger, then less and less real numbers are representable with a floating point types. This leads to a significant precision loss. This can be resolved by using BigDecimal.

The minimal and the maximal floating point values as well as the infinities are available as constants from the classes Double and Float.

Characters and Strings

The type 'char' is the primitive type for single characters. We write the character constants as a single symbol inside a single quotes. For example 'a', 'b' and 'c' are character constants. If we want to use the single quote itself as a constant then we must escape it, i.e. we write '\''. The backslash indicates that the next symbol must be treated specially. In this case the sequence \' replaces a single quote. Similarly '\\' is the constant for backslash. There are many other escape sequences, but the two most commonly used are '\b' and '\n'. The first forces the cursor in the terminal to move to the previous position, and the second moves the cursor to move to the next line.

Although the type 'char' is intended to be used only for representing characters, in practice it is actually an integer type. The reason is that computers represent characters as numbers, where each character is encoded with an unique code defined by the Unicode standard. For this reason all operations available for integers are available for characters as well. There is also an automatic conversion from 'int' to 'char' and vice versa. There is also the class 'Character' which provides a number of static methods for different operations with characters.

The strings are actually special kinds of objects and not really primitive types, but still there is a special syntax for strings in the language which makes them look like primitive types. Internally, the string is a sequence of characters. There are a few things that we want to make clear about strings here.

As we have seen the syntax for string constants is to write the text in double quotes. In addition we can use the same escape sequences as we have seen for characters. The double quote can be included in a string by using the escape sequence \".

The operator '+' is used for concatenation of strings. It can also be used to concatenate a string with the textual representation of an integer. This is a general mechanism and it can be used with any kind of objects. For every class, we can define the method 'toString' which generates the textual representation for an object of the same class. For example we can have:

public class Employee {
   private String name;

   public Employee(String my_name) {
      name = my_name;
   }

   public String toString() {
      return name;
   }
}

now we can use the 'toString' method for printing objects of class 'Employee':

Employee john = new Employee("John");
System.out.println("name: "+john);

It is often needed to parse a string as an integer or a floating point value. The Java libraries provide convenient methods for parsing values of the different types:
byteByte.parseByte
shortShort.parseShort
intInteger.parseInt
longLong.parseLong
doubleDouble.parseDouble
floatFloat.parseFloat

Boolean Operations

The name of the boolean type comes from the name of George Boole who developed the logic of truth values, also known as boolean algebra. A variable of type boolean can have only two values 'true' and 'false'. There are number of operators that compute a value of this type. For example the comparison operators between numbers:
a == bequal to
a != bnot equal
a < bless than
a > bgreater than
a <= bless than or equal
a >= bgreater than or equal

We can also combine several conditions into one by using a logical conjunction (&&) or disjunction (||). For example, if we want to test that a number is within a sertain range we can use:

(n >= 0 && n <= 10)

This condition checks whether the number 'n' is bigger than or equal to zero and less than or equal to 10, i.e. it must be in the range - [0; 10]. Similarly we can use the disjunction to check whether a number has one or another value. Example:

(n == 0 || n == 1)

checks whether n is equal to either 0 or 1.

The logical negation is implemented with the operator '!' in Java. If we want to check whether a number is not equal to 0 or 1 then we can use:

!(n == 0 || n == 1)

which is also equivalent to:

(n != 0 && n != 1)

The results from logical expressions can be assigned to boolean variables, but it is a lot more common to use them to control the flow of the program. This happens with the if-then-else construction:

if (a > b) {
  .... do this if the condition is satisfied
} else {
  .... otherwise do this
}

The 'else' clause is optional and it can be skipped, if there is no need to do anything when the condition is not satisfied.

Arrays

Although we haven't mentioned it so far, we have seen arrays already. Remember the definition of the 'main' method:

public static void main(String[] args) {
}

So far, we have intentionally been silent about the parameter args since this is a variable of type array. The following video from the course CS50 at Harvard explains the basic idea of arrays. The course is about the language C but the first 3 minutes in the video are also relevant for Java:

When we run an application, the operating system lets us to pass one or more parameters to it. For example, if we have the class Hello, then we can run it like this:

java Hello John Mary Jim

The first part, i.e. java Hello, is something that we have seen already; it just runs the Java interpreter and tells it to execute the class Hello. The new thing is that everything that we write after the class name is parsed as a sequence of words and passed as arguments to the main method in the class. Every word will be a different element in the array args. We can test this straight away:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello "+args[0]);
        System.out.println("Hello "+args[1]);
        System.out.println("Hello "+args[2]);
    }
}

If you run this code with the command line from above, you will see that the program prints the names 'John', 'Mary' and 'Jim' each on a new line.

In general you can define an array by adding square brackets after the type for its elements. In the following, the variables 'a', 'b' and 'c' are arrays with elements of type integer, string and double:

int[] a;
String[] b;
double[] c;

In order to access an element of the array, we need to use the variable name followed by a number in square brackets. The number tells which element from the array you want to access. Each element is uniquely identified by its position. When you run the Hello class, its main method will get as parameter an array filled in this way:
elementJohnMaryJim
index012

Note that the indices start from zero. If we want to access the first element, then we must use index zero. If we want the third element, then we need index two.

The example from above already shows one thing that you cannot do without arrays. When you write your program you cannot know how many arguments the user is going to supply. Even if you know it, the compiler does not know your intentions and the interpreter cannot check it. Instead the interpreter collects all arguments that are available, and packs them in an array which is after that given to you. It is your responsibility to verify whether the user has typed something that you can interpret.

The first thing that you need to know is how many parameters there are. Every array has a public final variable called 'length' which tells you its size. The example above presupposes that there are exactly three arguments and it will crash if there are less. If you supply more, then the rest will be ignorred. Try to call it like this for instance:

java Hello John

it will crash with the error:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1
 at Hello.main(Hello.java:4)

Any attempt to access an element with index which doesn't exist will lead to a similar error.

The arrays are usually used in loops. In our example the user can supply many names, why should we restrict ourself to only three? We can iterate over all names and say hello to all of them:

public class Hello {
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
           String arg = args[i];
           System.out.println("Hello "+arg);
        }
    }
}

There should not be anything surprising here. There is a loop which iterates over all arguments and prints hello for each of them. The only thing that deserves a comment here is the termination condition for the loop. Note that it iterates until the index 'i' is smaller than the length of the array, i.e. the last element has index equal to the length minus one. If we had the condition i <= args.length, then the program will try to access the element args[args.length] which does not exist since the indices for the array start from zero. A sequential iteration over all elements of an array is a very common pattern. Java has a more convenient syntax for this purpose:

public class Hello {
    public static void main(String[] args) {
        for (String arg : args) {
           System.out.println("Hello "+arg);
        }
    }
}

This is a special form of the 'for' loop which lets you to iterate over an array without thinking about the indices and the lengths. The variable that we introduce in the loop will be assigned directly to the value of the element and not to its index. This saves you one more line of code. The disadvantage of this kind of loop is that now you don't know the current index for every iteration.

So far we have only used an existing array but of course we must be able to create our own. The arrays are just objects and they can be created with the operator new. For example, if I need an array with 10 integers, then I can create it in the following way:

int[] a = new int[10];

Note that when we declare the type of the variable for the array, we never specify its length. We must specify the length when we create the array, however. In this case the length of the array is a constant but it does not have to be. It could be any expression that is computed at runtime. After we create the array, its elements will be automatically initialized to a default value. If the elements are of integer or floating number type, then the default value is zero. If the elements are booleans, then the default is 'false', and if this are objects then the default is 'null'.

Sometimes we know in advance both the length of the array and its the values that we want to assign. In this case we can initialize it in a simpler way:

String[] alphabet = {"alpha", "beta", "gamma"};

The number of expressions between the curly braces determine the length of the array and the values for the elements of the array will be set accordingly. Of course the other way to set the values in the array is to use assignments:

alphabet[0] = "alpha";
alphabet[1] = "beta";
alphabet[2] = "gamma";

For doing mathematical calculations we sometimes need arrays with two and more dimensions. Unfortunately more than one dimension is not directly supported in Java. Instead you can use an array containing other arrays. You can create a dwo dimensional matrix in this way:

double[][] matrix = new double[3][3];

but this will actually create one dimensional array of length 3, whose elements are other arrays of length three containing the real numbers. We can verify this by trying to compile the code:

double[][] matrix = new double[3][3];
matrix[0] = null;

The expression matrix[0] refers to the nested array representing the first row from the matrix. Since this is just an object reference, the compiler let us to set it to zero. This will essentially destroy the illusion that we have a two dimensional array.

Despite that Java doesn't have real two dimensional arrays, the replacement serves the same purpose. We can access the elements of the matrix by a sequence of two indices, i.e. matrix[0][0] refers to the upper left corner and matrix[0][2] is the upper right corner.

Objects vs Primitive Types

It is important to note that the primitive types mentioned above are not objects. The difference is that the objects are allocated in the memory and the variables are only references to the place where the object is located. When we have a variable of primitive type, then the variable itself contains the value. This leads to some differences in the way in which we work with the primitive types: It is not possible to call a method for a primitive type. For example it is not possible to say:

10.toString()

instead we use the static method of class Integer:

Integer.toString(10)

All object variables can have the value 'null' which indicates that the variable does not point to any object yet. This is not possible for primitive types because then the variable contains the value itself.

Classes like Integer, Character, Double and Float are the object oriented counterparts to the primitive types. Indeed we can convert between the two:

int n = 10;
Integer nobj = null;
nobj = new Integer(n);
int n2 = nobj.intValue();

The difference between primitive types and classes is one of the main criticisms against Java. It is sometimes said that Java is not a pure object oriented language.

Arguments by reference

It is important to remember that there is a difference in the way in which objects and primitive values are passed as arguments to methods. Look at the following method:

public static void swap(int x, int y) {
    int tmp = x;
    x = y;
    y = tmp;
}

It seems to swap the values of the two arguments x and y but this happens only locally. If you call the method as swap(a,b) then this will not exchange the values of the variables a and b. Instead their values will be copied as values of the local variables x and y in the method swap. After that what we swap is the copied values which has absolutely no effect on the values of the original variables.

This is different if we operate with objects or arrays. The following version of the method will exchange the values in the actual array:

public static void swap(int[] arr) {
    int tmp = arr[0];
    arr[0] = arr[1];
    arr[1] = tmp;
}

If we call it like this:

int[] a = {1, 2};
swap(a);

then the final value of a will be {2,1}. The reason is that when we have an argument which is an object or an array then we only pass a reference to it and we don't copy it.

Equality

The difference between objects and primitive types also shows up when we do equality testing. Unfortunately understanding the right way to do equality testing in Java could be very confusing. The equality sign (==) which works in the expected way for primitive types like integers and floating numbers, is not doing the thing that you might expect for objects.

With objects the equality actually doesn't check whether the two objects have the same values but whether we have two different references to the same object. This is especially confusing with strings, since a naive test might suggest that the equality works as normal. Try the following program:

String a = "hello";
String b = "hello";

System.out.println(a == b);

If you run it you will get the expected result, i.e. 'true', but it is true for the wrong reason. Now try this instead - compile and run the following program:

public static void main(String[] args) {
	System.out.println(args[0] == args[1]);
}

You run the program with two arguments in the command line and it prints whether the arguments are equal. Run the program with two equal arguments. It doesn't matter how careful you are with the typing, even if you write exactly the same strings the program will always print false.

What happens here is that the arguments will be read and the interpterer will create one string object for each argument. args[0] will point to the first one and args[1] to the second one. After that the equality operator will simply check whether args[0] and args[1] point to the same object. Even if you have typed exactly the same strings, they will be stored in different objects and the result will be false. It is more misterious why we got true in the first example. The reason is that when we have strings as literal constants, then Java collects all constants with the same value and it creates only one object for every constant. This means that in the first example the variables 'a' and 'b' will actually point to the same object.

The discussion above suggests that the operator (==) has a limited use for objects. It is useful only when we really want to compare references to objects and not the objects themselves. Think about the 'Counter' class from the previous lecture for instance. Even if we have two counters which are currently showing the same number, we still want to be able to distinguish them. The next time when someone calls the increment method of one of them, they will no longer have equal values. For immutable objects such as string, we most of the time need an equality based on the content. For this purpose the class String provides the method 'equals' which compares the actual values. This means that the program:

public static void main(String[] args) {
	System.out.println(args[0].equals(args[1]));
}

will actually print the expected result. Using (==) instead of 'equals' is a very common mistake which even experienced Java programmers might do by mistake. The class String also provides the method 'equalsIgnoreCase' which does case insensitive comparison. If you use this method, then for instance "Hello" and "HELLO" will be considered as equal.

The 'equals' method is actually more general and it can be added to any class to provide comparison based on the values and not on the references. The following is an example of an 'Employee' class which defines an equals method where the employees are compared by name:

public class Employee {
   public String name;

   public Employee(my_name) {
       name = my_name;
   }

   public boolean equals(Object obj) {
      Employee other = (Employee) obj;
      return (name.equals(other.name));
   }
}

Note the casting in the beginning of the 'equals' method. The argument of the method is of type Object and this is the object with which we do the comparison. In our case we can compare employees only with another employee and this is why we cast the object to an employee.

Type Casts

Sometimes we need to convert from one type to another. For example we may want to convert an integer to a real number and vice versa. When the conversion does not imply loss of information then it happens automatically. We just assing a value of one type to a variable of another type and the compiler automatically takes care of inserting an appropriate conversion. For example:

int x = 3;
double y = x;

This will be perfectly fine for the compiler, since the integer in x can be safely converted to a real number without loss of precision. The converse however:

double y = 3.14;
int x = y;

will not be permitted since the number 3.14 cannot be made into integer without rounding it to 3, i.e. we lose precision. For that reason the compiler will not do the conversion unless if it is explicitly asked to do so:

double y = 3.14;
int x = (int) y;
System.out.println(y);

This will print 3. The casting from 'double' to 'int' truncates all digits after the decimal point and then the result is the whole number before the point. If you want the usual rounding procedure then you must use the method Math.round.

We can also do the opposite assignment, i.e. we can assign a reference to Counter to a variable of type SerialCounter but this works only if the reference is actually a SerialCounter. Since the conversion is not always possible the casting is not automatic. Instead we have to write it like this:

SerialCounter serial_counter = (SerialCounter) counter;

If the reference in counter is not actually a serial counter, then the above operation will generate a runtime error.

We use both (int) and (SerialCounter) as operators which enforce conversions from one type to another for which the compiler cannot determine automatically that those are safe.

This can be used for instance to generate a random integer in a given range. Lets say that you need an integer from 0 to 10. You can get it by doing:

int x = (int) 10*Math.random();