Session 14: Primitive types

Integer primitive types (Section 9.2)
    The types
    Literals
Floating-point primitive types (Section 9.3)
    The types
    Literals
The character type (Section 9.4)
    Unicode
    Characters as integers
    Character class methods
Type conversions (Section 9.5)
    Implicit conversion
    Explicit conversion
Object types (Section 3.1)
    Properties
    Creating new instances
    Behaviors

Remember how we said there were only four primitive types in Java: char, int, double, and boolean? That wasn't the whole truth.

Integer primitive types

Textbook: Section 9.2

The types

Java actually has four primitive types for representing integer values.

byterepresents an 8-bit integer value
shortrepresents a 16-bit integer value
intrepresents a 32-bit integer value
longrepresents a 64-bit integer value
All of these are signed numbers, being represented in two's-complement form. Thus, a byte can represent numbers from -128 to 127. (The class Byte defines MIN_VALUE and MAX_VALUE constants, for you to use when you want to refer to the smallest popssible value.)

Generally you should just stick with int, but every once in a while one of the other types is justified. Some examples of situations where they can be important:

These occasions are quite rare, but they do happen.

Literals

A literal is the character representation of an individual value that you can include in the code. For example, ``123'' is an int literal, representing the integer value 123.

By default, when you type a literal in Java containing only digits, it is an int literal. You can also use an octal representation, prefixed by an initial 0. For example, ``015'' is an octal representation of the number 15(8)=13(10). And you can use a hexadecimal representation by prefixing the number with an initial 0x or 0X: ``0xA0 represents A0(16)=160(10).

You can create a long literal by appending an L to an int literal. (You can also use l, but that's confusing, as it looks too much like a 1.) 10000000000000L represents a long value - and it's a compiler error if you leave off the L, as the number doesn't fit into an int.

Floating-point primitive types

Textbook: Section 9.3

The types

Java actually has four primitive types for representing integer values.

floatrepresents a 32-bit floating-point value
doublerepresents a 64-bit floating-point value
Both use the IEEE formats, similar to what you would have seen in CS 150.
typemaximumprecision
float 3.40×1038 6 digits
double 1.79×10308 15 digits

There are constants defined in the Float and Double classes that represent extremes for the types.

Generally you should just stick with double, but occasionally you'll need a float. The most frequent reason for this would be a huge array of floating-point numbers, when spending 8 bytes on each number isn't acceptable, and the added precision isn't needed.

Literals

Any numerical representation containing a decimal point (3.14) or an exponent (3e8) is a floating-point literal. By default, it is a double, but you can append a f (or F) to specify it as a float literal.

You can also append a d or f to what would otherwise be an int literal, making it either a double or float literal: 3d would represent the double value 3.0. (But it's better to just put in the decimal point.)

There's no literal for infinity, negative infinity, or NaN (not a number), but the IEE format permits these values. Instead, you can use Java's built-in constants Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY, and the class method Double.NaN, which tells whether a double value represesents NaN.

The character type

Textbook: Section 9.4

Unicode

There's only one character primitive type - the char. In older languages, the character type is an 8-bit type, using the ASCII values. (Or another standard, on some odd computers that few people use anymore.) But in Java, char is a 16-bit type, using the Unicode representation.

Unicode is a newer standard that uses 16 bits. By allowing the extra 8 bits, the number of representable characters expands vastly. This permits the inclusion of many alphabets, enabling a Java program to work across many languages. The Unicode standard still includes ASCII for the first 128 values. (Actually, they adjust many of the control characters in the ASCII code, many of which have long been useless, but the visible characters remain the same.) But it includes many other characters - including the alphabets of Cyrillic, Greek, Hebrew, Arabic, Cherokee, and many others, as well as many odd symbols.

Characters as integers

In Java, the character type is actually another integer type. This permits you to do arithmetic on letters. For example, you could do the following to print out the alphabet.

for(char ch = 'A'; ch <= 'Z'; ch++) IO.println(ch);
Or you could have the following to convert a hexadecimal digit into its value.
public static int convertHexadecimalDigit(char ch) {
    // note that using Character.digit(ch, 16) is superior to this.
    if(ch >= '0' && ch <= '9') return ch - '0';
    else return ch - 'A';
}
You can even multiply or divide characters, though there's never a reason to actually do this.

Character class methods

Actually, the usefulness of doing arithmetic on characters is severely hampered by the fact that Java provides a number of built-in methods for working with characters. These are preferable to doing arithmetic, both because it makes the code easier to read and because it keeps your code more portable to other languages.

For example, Character.isLetter(ch) returns true if ch represents a letter of any language. There are many such methods in Java: isDigit, isWhitespace, and isLowerCase, for example.

There are also useful conversion routines: digit(ch, radix) converts from a digit to its integer representation (radix represents the base), and forDigit(i, radix) goes the other way. You should use digit(ch, 16) instead of the convertHexadecimalDigit() I defined earlier. Also, the methods Character.toLowerCase(ch) and Character.toUpperCase(ch) convert characters between their capital and lower-case equivalents.

Type conversions

Textbook: Section 9.5

Implicit conversion

We've already seen that Java will automatically convert an int to a double when needed. For example, if you type ``9.0 / 5,'' Java automatically converts 5 into a double before doing the division.

In general, Java will happily assume you meant to do a conversion whenever it considers the new type to be ``broader'' (i.e., less restrictive) than the old type. Java observes the following hierarchy of types.

 byte
  |
short    char
    \   /
     int        narrow
      |           |
     long         V
      |         broad
    float
      |
    double
The following example illustrate.
int i = 'A';     // OK: Sets i to 65, the Unicode value of 'A'
float f = 3;     // OK: Even though 3 is an int literal, f holds 3.0
long k = 2.0;    // ERROR: 2.0 is a double, broader than a long.
int j = 5L;      // ERROR: 5 is a long, broader than an int.
These types of conversions are called numeric promotions.

The same rules apply in parameter passing. There's no problem with passing an int argument into a method that takes a double parameter - Java will simply automatically convert it.

And the same rules apply with arithmetic, except that Java doesn't include the concept of byte or short arithmetic. So if you add two byte values, they'll get promoted into ints before the addition takes place.

Explicit conversion

So what do you do in those cases where you want to convert down the hierarchy? For example, if I have a double value, how can I convert it into an int value? Java requires you to explicitly mark these conversions, so that you don't accidentally lose information. (When you convert down the hierarchy, there's a risk of problems, since the value may not be a valid value for the new type. For example, though 200 is a perfectly good int value, it's not good as an byte value.)

You do these explicit conversions using the casting operator. It's a unary operator that you can place in an expression; you type the name of the type ot which you want to convert in parentheses. For example:

int j = (int) IO.readDouble();
This reads a floating-point number from the user, but then it applies the casting operator to convert the return value of IO.readDouble() into an int. When you convert a floating-point value to an integer type, any fractional value is dropped, so anything the user types after the decimal point is lost here.

The casting operator is very high in the hierarchy of operators - it ranks above the multiplication level, on the same level as unary negation.

int i = 1, j = 2;
double x = (double) i / j;
In the above code, the casting operator only applies to i - converting the value 1 into 1.0. Because one side of the division is a double value, the other side is implicitly converted before the division takes place, so that you get 0.5 as a result.

Say you wanted to print a random integer between 1 and 6. The following accomplishes it.

IO.println((int) (6.0 * Math.random() + 1));
Here the parentheses around ``6.0 * Math.random() + 1'' were important, as otherwise the casting operator would apply only to the 6.0, and the effect would be lost when it is immediately (implicitly) converted to a double again so that it can be multiplied by the double value returned by Math.random(). As it is, the random number between 0.0 and 1.0 returned by Math.random() is multiplied by 6, giving a random double between 0.0 and 6.0. We add 1 to it, moving the range to being between 1.0 and 7.0. We typecast this to an int; any fractional part is lost, and the result will be an integer between 1 and 6.

Object types

Textbook: Section 3.1

We've seen this, but it's been causing problems, and after some deliberation, I thought of a new way to explain it. I fear it might confuse people more than it helps, but maybe it might help.

Long ago, philosophers were fascinated by words and what they meant. Plato had a philosophy that there were two realities: the tangible world, and the world of forms. A form is an abstract object that represents a category of objects. For example, there is a Human form, a category to which we all belong. For Plato, forms are not merely abstractions or categories, however: They are real - in fact, they are more real than tangible objects, since a tangible object may cease, but a form will never pass away.

For example, when we use a word, like chalk, we are referring to the abstract form Chalk. (I'm capitalizing it, because it is a name for a particular form. There's only one form for chalk, namely Chalk.) In our tangible world, there are many objects that participate in Chalkness - you can think of them as shadows of the Chalk form, projected into the tangible world where we can see and feel them. But there is only one Chalk form.

Java uses a slightly different terminology than Plato. Whereas Plato uses the word form, Java uses the word class. And whereas Plato might talk about concrete instances of a form to refer to objects in the tangible world, Java calls these instances or objects.

As Java programmers, we tell the computer about new forms using the Java language: That is, we define new classes. When we define a class like Ball, there is only one such class - just as there is only one form Chalk.

public class Chalk {
    // definition of Chalk form goes here
}

Properties

Notice that a particular form has certain properties. For example, all chalk is white (imagine that colored chalk was unfathomable); and, indeed, Plato would assert that the Chalk form is white. Such shared properties are class variables. (Calling it a variable is a slight misnomer - a platonic form is unchanging and permanent, so it's really a constant. Similarly, in Java, a class variable is usually declared final, making it too unchanging.)

On the other hand, tangible instances participating in the Chalk form - that is, individual pieces of chalk - have some properties too. Each individual piece of chalk has its own length, for example. Such a property is an instance variable.

public class Chalk {
    static final Color color = Color.white; // a class variable!

    double length; // an instance variable!

    // rest of definition comes here
}

Creating new instances

When a new piece of chalk is created, its instance properties will have fresh, new values. In Java, we define what these fresh values will be via what Java calls the constructor method.

    // part of the Chalk class definition: a constructor method
    Chalk() {
        length = 8.0; // 8 cm
    }
Now, whenever we generate a brand new piece of chalk, it creates a piece of chalk whose length property indicates that it is 8 cm long.

Behaviors

A form also has particular behaviors. These are called methods in Java. (Method wasn't a particularly good word choice, but we're stuck with it. If they used the word behavior, we'd confuse all those Canadians who can't spell properly.)

A form might itself do something. Plato would get upset about this - remember that a form is an unchanging, immortal entity. But we might ask the form something innocuous, like we might want a method so that we can ask if a piece of chalk would dissolve in some liquid liq. This is a property of the form Chalk, as it is an essential element to how chalk behaves. Thus it would be a class method. This particular class method would take a parameter (which we are calling liq for the purposes of defining the method) and return a boolean value.

    // part of the Chalk class definition: a class method
    static boolean willDissolve(Liquid liq) {
        if(liq.isAcid()) { // chalk dissolves in acid
            return true;
        } else {
            return false;
        }
    }

There are some things that only particular instances can do. For example, while the form Chalk cannot itself draw a line, we might ask a particular piece of chalk to draw a line. This would be an instance method. This particular instance method would take some parameters saying where to draw the line, and it wouldn't return anything.

    // part of the Chalk class definition: an instance method
    void draw(Graphics g, int x0, int y0, int x1, int y1) {
        if(length > 0.0) {
            g.setColor(color); // we're referring to our class variable
            g.drawLine(x0, y0, x1, y1);
            length -= 0.01; // chalk gets slightly shorter
        }
    }

Here's our complete definition of the Chalk form, with a couple of additional instance methods thrown in too.

public class Chalk {
    // class variable definitions
    static final Color color = Color.white;

    // class method definitions
    static boolean willDissolve(Liquid liq) {
        if(liq.isAcid()) { // chalk dissolves in acid
            return true;
        } else {
            return false;
        }
    }

    // instance variable definitions
    double length; // an instance variable!

    // constructor method definitions
    Chalk() {
        length = 8.0; // 8 cm
    }

    // instance method definitions
    void draw(Graphics g, int x0, int y0, int x1, int y1) {
        if(length > 0.0) {
            g.setColor(color); // we're referring to our class variable
            g.drawLine(x0, y0, x1, y1);
            length -= 0.01; // chalk gets slightly shorter
        }
    }

    int getLength() {
        return length;
    }

    Chalk split() {
        Chalk ret = new Chalk();
        length /= 2.0;
        ret.length = length;
        return ret;
    }
}