Layers of Logic

2.1

Variables, Types, Literals, Keywords

Give a value a name, declare what that name can hold, and let the compiler check each use before the program runs.

Core22 min read5 exercises
01

Previously on

In Section 1.2 you compiled source code and ran the resulting program on a JVM.

Those programs printed fixed text. This section adds named values, so a program can calculate with data and report a result.

02

The problem

Write a program that adds 2 and 3. The calculation needs both values, and the result needs somewhere to go.

public class AddTwoNumbers {
    public static void main(String[] args) {
        int firstNumber = 2;
        int secondNumber = 3;
        int sum = firstNumber + secondNumber;

        System.out.println(sum);
    }
}

Output:

5

firstNumber, secondNumber, and sum are variables. Each declaration gives Java three facts:

  • the variable’s type
  • the variable’s name
  • its initial value

The compiler uses the type to reject operations that cannot be valid. It also knows how much numeric range and precision the primitive type provides.

03

The idea

Read a declaration from left to right

int score = 91;
Part Meaning
int the declared type
score the identifier that names the variable
= assignment
91 an integer literal
; the end of the statement

A declaration introduces the variable. An initializer supplies its first value.

int attempts;       // declaration
attempts = 1;       // assignment
int score = 91;     // declaration with an initializer

The declared type does not change later:

int score = 91;
score = 95;         // valid: another int value
// score = true;    // compile-time error

This is static typing. Java checks types during compilation, before these statements run.

Identifiers and keywords

An identifier is a name chosen by the programmer. Use names that state what a value means.

double unitPrice = 12.50;
int quantity = 4;
double total = unitPrice * quantity;

Identifiers are case-sensitive. total, Total, and TOTAL are different names.

Common identifiers use letters, digits, _, and $, and cannot start with a digit. Java also accepts many Unicode letters and currency symbols. It reserves keywords such as class, int, public, and return for the language grammar.

int classSize = 30;  // valid
// int class = 30;   // invalid: class is a keyword

The eight primitive types

Java defines exactly eight primitive types.

Family Type Language-defined value set
signed integers byte 8 bits, -128 through 127
signed integers short 16 bits, -32,768 through 32,767
signed integers int 32 bits, -2,147,483,648 through 2,147,483,647
signed integers long 64 bits, -2^63 through 2^63 - 1
binary floating point float 32-bit IEEE 754, about 6 to 9 significant decimal digits
binary floating point double 64-bit IEEE 754, about 15 to 17 significant decimal digits
UTF-16 code unit char unsigned 16 bits, \u0000 through \uFFFF
logical boolean true or false; storage size is unspecified

The integer widths and ranges are fixed by the Java language. They do not change between a 32-bit laptop and a 64-bit server.

char stores a code unit

This program shows both the useful case and the boundary:

public class CharDemo {
    public static void main(String[] args) {
        char latin = 'A';
        char devanagari = '\u0915';
        String rocket = "🚀";

        System.out.println(latin);
        System.out.println(devanagari);
        System.out.println(rocket.length());
        System.out.println(rocket.codePointCount(0, rocket.length()));
    }
}

Output:

A

2
1

The rocket is one Unicode code point represented by two UTF-16 code units. String.length() counts code units, so it returns 2 here.

Choose a default, then justify exceptions

Use int for ordinary whole-number calculations and double for ordinary binary floating-point calculations. Choose another type when its range, precision, API contract, storage density, or domain meaning gives a concrete benefit.

float is useful in some graphics and large numeric arrays. byte is useful for encoded binary data. long is required when int cannot cover the range. Money and other exact decimal quantities need a decimal representation such as BigDecimal, not double.

04

Under the hood

Going deeper

Bits determine the integer ranges

One bit has two patterns. With n bits there are 2^n patterns.

7060504031201100
= 10
The 8 column and the 2 column are set, so the unsigned magnitude is 10.

For a signed byte, the same 256 patterns are interpreted as -128 through 127. Section 2.2 explains the two’s complement rule that makes that mapping work.

Literal spelling has a type

Values written directly in source are literals. Their spelling controls their type.

int decimal = 42;
int binary = 0b0010_1010;
int octal = 052;
int hexadecimal = 0x2A;

long worldPopulation = 8_100_000_000L;
float load = 0.75f;
double avogadro = 6.022_140_76e23;
char grade = 'A';
boolean ready = true;

All four int variables above contain 42. The prefixes change how the source digits are read. They do not create different runtime integer types.

An unsuffixed decimal integer literal normally has type int. Add L when the value needs long. An unsuffixed floating-point literal has type double. Add f or F for float.

Single quotes create a char literal. Double quotes create a String, which is not a primitive type.

Local variables must be initialized before use

Java does not let a method read an uninitialized local variable.

public class Initialization {
    public static void main(String[] args) {
        int result;
        // System.out.println(result); // compile-time error

        result = 12;
        System.out.println(result);
    }
}

This rule is called definite assignment. Fields receive default values, but local variables do not. Fields arrive with classes in Phase VI.

Fixed-width integer arithmetic can overflow

An int always has 32 bits. Java does not add a 33rd bit when a result needs one.

public class OverflowDemo {
    public static void main(String[] args) {
        int largest = Integer.MAX_VALUE;
        int wrapped = largest + 1;

        System.out.println(largest);
        System.out.println(wrapped);

        try {
            System.out.println(Math.addExact(largest, 1));
        } catch (ArithmeticException ex) {
            System.out.println("overflow detected");
        }
    }
}

Output:

2147483647
-2147483648
overflow detected

Ordinary integer operators wrap modulo 2^32; they do not report overflow. Methods such as Math.addExact are available when overflow must become an error.

05

What it costs

Types let the compiler reject invalid assignments and document the shape of data. The constraint also means you must choose ranges and conversions before running the program.

Narrow integer types do not make arithmetic narrow. Java promotes byte, short, and char before most arithmetic, which Section 2.3 covers. Use them when their storage or API meaning matters, not as a default calculation type.

Binary floating-point types trade exact decimal representation for a wide range and fast hardware operations. That trade becomes visible in the next section.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. Why does `byte` stop at 127 when eight bits provide 256 patterns?

    Show the answer

    byte is signed. Its 256 patterns represent -128 through 127: 128 negative values, zero, and 127 positive values. The exact interpretation is fixed-width two's complement, which Section 2.2 derives.

  2. Why does `float price = 3.99;` fail even though 3.99 is inside the range of `float`?

    Show the answer

    The literal 3.99 has type double. Assigning it to float is a narrowing conversion, so Java requires an explicit cast or an f suffix. Write float price = 3.99f;, or keep the greater precision with double price = 3.99;.

  3. Does `char` always hold one complete visible character?

    Show the answer

    No. A char is one unsigned 16-bit UTF-16 code unit. Many common characters use one code unit, but some Unicode characters use a surrogate pair and therefore need two char values. Text normally belongs in a String.

  4. How many bytes does a Java `boolean` occupy?

    Show the answer

    The Java language does not specify a storage size for boolean. It specifies only the values true and false. Object layout, fields, arrays, and JVM implementations can represent booleans differently, so do not derive memory totals from a claimed one-bit or one-byte size.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

5 exercises105 pointsabout 110 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

All Eight, Printed

Warm up·15 min·10 points

checkedex-2-1-a

Declare one variable of every primitive type, then print them all.

This is deliberately mechanical. Do it without looking at the tables in the section if you can, then check yourself against them.

Two of the eight will refuse to compile on your first try. That is not you failing. That is the literal type rule from this section catching you, exactly as it will catch you in real code. Read the error, work out which rule it is, fix it.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Primitives {
    static byte aByte()
    static short aShort()
    static int anInt()
    static long aLong()
    static float aFloat()
    static double aDouble()
    static char aChar()
    static boolean aBoolean()
}

Eight methods, one per type, each returning a sensible value. The return types are the exercise: the checks assign each answer to a variable of that exact type, so a wrong type will not build.

What your program must do

  • Return a value of each of the eight primitive types
  • Make the short too big for a byte and the long too big for an int
  • Give the float and the long the suffix each one needs
  • Print all eight and say which two needed a suffix and why
Primitives.java
public class Primitives {

    // One method per primitive type. Return a sensible value from each.
    // Two of them need a suffix on the literal. Two of them need particular quotes.

    static byte    aByte()    { return 0; }     // TODO
    static short   aShort()   { return 0; }     // TODO: big enough that a byte could not hold it
    static int     anInt()    { return 0; }     // TODO
    static long    aLong()    { return 0; }     // TODO: big enough that an int could not hold it
    static float   aFloat()   { return 0; }     // TODO
    static double  aDouble()  { return 0; }     // TODO
    static char    aChar()    { return 0; }     // TODO
    static boolean aBoolean() { return false; } // TODO

    public static void main(String[] args) {
        // TODO: print all eight, one per line
    }
}

Sample run

It prints
byte   smallCount     = 100
short  mediumCount    = 30000
int    population     = 1400000000
long   distanceInKm   = 9460730472580800
float  roughPrice     = 3.99
double preciseValue   = 3.14159265358979
char   grade          = A
boolean isActive      = true
Hint 1
A plain decimal like 3.99 is a double, so a float needs 3.99f. A plain whole number is an int, so a long past 2.1 billion needs 9_000_000_000L.
Hint 2
A char takes single quotes. Double quotes make a String, which is not a primitive at all.
Hint 3almost the answer
Underscores are allowed in numbers and the compiler ignores them, so 9_000_000_000L reads better than the same digits run together.
What this is really testing

Whether you can declare each primitive type without looking it up. This is the one piece of pure memorising in the phase, and doing it by hand once is faster than looking it up forty times later.

B

Watch an int Overflow

Real work·20 min·20 points

checkedex-2-1-b

The section said that overflow is silent. Go and see the silence for yourself.

Take the largest number each type can hold, add one, and print the result. Write down what you expect first. Most people guess zero, or an error. It is neither.

Then fix the int case so it gives the right answer, and pay attention to where the fix has to go. Putting the cast in the wrong place is a real bug that looks like a fix, and it appears in production code more often than anyone would like.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Overflow {
    static int intOverflow()
    static byte byteOverflow()
    static short shortOverflow()
    static long longOverflow()
    static boolean threwAnything()
}

Each of the first four adds one to that type's maximum and returns the result. threwAnything reports whether adding one to Integer.MAX_VALUE raised anything at all.

What your program must do

  • Predict all four results before running, then implement them
  • Report whether anything was thrown
  • Say why the byte version needs a cast and the int version does not
  • Say what this means for a program that silently passes the maximum
Overflow.java
public class Overflow {

    // Add one to each type's maximum and return what you get.
    // PREDICT all four before running.

    static int   intOverflow()   { return 0; }  // TODO
    static byte  byteOverflow()  { return 0; }  // TODO: this one needs a cast. Work out why
    static short shortOverflow() { return 0; }  // TODO
    static long  longOverflow()  { return 0; }  // TODO

    // Did anything get thrown? Try it and find out.
    static boolean threwAnything() {
        return true; // TODO
    }

    public static void main(String[] args) {
        // TODO: print all four, plus whether anything was thrown
    }
}
Hint 1
Every one of them wraps round to that type's most negative value. Adding one to the largest int gives -2147483648.
Hint 2
Byte.MAX_VALUE + 1 is an int expression, because Java promotes small types before arithmetic. So it has to be narrowed back with a cast. Integer.MAX_VALUE + 1 is already an int, so it wraps on its own.
Hint 3almost the answer
The ordinary + operations in this exercise throw nothing. There is no warning and no crash. Use Math.addExact when overflow must raise ArithmeticException.
What this is really testing

Whether you understand that overflow is silent. No exception, no warning, no crash. Your program keeps running and your number is now wrong, and this is one of the nastiest bug shapes there is.

C

Six Broken Declarations

Real work·20 min·20 points

checkedex-2-1-c

Six declarations. All six are broken. Each one is broken for a different reason.

Fix them so the file compiles, without changing what any line is trying to do.

The point is not the fixing, which is easy once you see it. The point is the error messages. Read each one properly before you fix it, and connect it to a rule from this section. Do that now, deliberately, and these errors will cost you seconds instead of minutes for the rest of your life.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Broken {
    static long distance()
    static float price()
    static char grade()
    static int classSize()
    static byte count()
    static boolean ready()
}

Six methods holding the six broken declarations, each fixed. The values stay the same wherever the value was legal. One of them cannot keep its value, and finding out which is part of the exercise.

What your program must do

  • Fix all six and name the rule behind each error before changing it
  • Say which one cannot keep its original value, and what it becomes
  • Say which two are about literals and which one is about a keyword
  • Print all six
Broken.java
public class Broken {

    // Six declarations, six different rules broken. Fix each one.
    // Read the compiler error first, and name the rule before you change anything.

    static long    distance()  { return 9460730472580800; }  // TODO
    static float   price()     { return 3.99; }              // TODO
    static char    grade()     { return "A"; }               // TODO
    static int     classSize() { return class; }             // TODO
    static byte    count()     { return 200; }               // TODO
    static boolean ready()     { return 1; }                 // TODO

    public static void main(String[] args) {
        // TODO: print all six once they compile
    }
}
Hint 1
Two of them are about the type of a literal. A plain whole number is an int and a plain decimal is a double, so a big long needs L and a float needs f.
Hint 2
class is a reserved word, so it can never be a variable name. Rename it. "A" in double quotes is a String, and a char needs single quotes.
Hint 3almost the answer
200 does not fit in a byte. Casting it keeps the low eight bits and reads them as signed, which gives -56. That is the same rule as Section 2.2, and it is why the cast is you saying you accept the loss.
What this is really testing

Whether you can read a compiler error and know which rule it comes from. Six errors, six different rules, and every one of them is something you will type by accident within your first month.

D

The Registry Begins

Real work·25 min·25 points·The Registry

checkedex-2-1-d

This is the first version of The Registry, a system you will keep coming back to across the whole course.

Right now it is nothing but a handful of loose variables in main, describing one unit. That is deliberate. It should feel clumsy.

In Phase VI you will bundle these into an object and see why. In Phase VII you will hide them behind methods. In Phase VIII you will give the whole thing proper identity, so two units can be compared correctly. Each time, you will come back to this file and see how far you have moved.

For now, one job: pick the right type for each fact, and be able to say why. Do not use int for everything. The exercise is the choosing.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Registry {
    static String unitName()
    static int unitId()
    static double readiness()
    static boolean active()
    static char classification()
    static long serialNumber()
    static byte crewSize()
}

Seven fields of the first Registry unit, each with a type you chose on purpose. The serial number must be past 2,147,483,647 and the readiness must carry a fraction, so both choices mean something.

What your program must do

  • Give every field a value that fits its type
  • Make the serial number past 2,147,483,647 so the long is a real decision
  • Give readiness a fraction so the double is a real decision
  • Write one line per field saying why that type and not the next one up
Registry.java
public class Registry {

    // One unit in the registry. Choose each type deliberately and be ready to defend it.
    // The serial number is past 2,147,483,647. The readiness has a fraction.

    static String  unitName()       { return ""; }    // TODO
    static int     unitId()         { return 0; }     // TODO
    static double  readiness()      { return 0; }     // TODO
    static boolean active()         { return false; } // TODO
    static char    classification() { return 0; }     // TODO
    static long    serialNumber()   { return 0; }     // TODO
    static byte    crewSize()       { return 0; }     // TODO

    public static void main(String[] args) {
        // TODO: print a tidy record of this unit
    }
}
Hint 1
classification is one letter, so char beats String. crewSize is never above 127, so byte says something a plain int would not.
Hint 2
The serial number needs the L suffix once it goes past 2.1 billion, or the literal will not compile.
Hint 3almost the answer
The defence is the exercise. "int because it always fits and is the default" is a real answer. "int because I always write int" is not, and the difference is the habit this is building.
What this is really testing

Whether you can choose types on purpose instead of typing int everywhere. Also this is the first version of a program you will keep rebuilding for the rest of the course, and it is meant to look primitive right now.

E

Work Out the Ranges Yourself

Hard·30 min·30 points

checkedex-2-1-e

You have been given four ranges in a table. Tables get forgotten. Formulas do not.

Work out the minimum and maximum for byte, short, int and long on paper, starting only from how many bits each one has. Then run the program and see whether you were right.

If your derivation is off by one somewhere, that is the most useful outcome you could have. The off-by-one is where zero lives, and understanding that is the difference between reciting a range and knowing it.

Finish by writing the general formula and its representation limit in your own words. A method returning long cannot return the positive counts 2^63 or 2^64. That limit belongs to the return type, not to the mathematics.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Ranges {
    static long minOf(int bits)
    static long maxOf(int bits)
    static long unsignedMaxOf(int bits)
    static long countOf(int bits)
}

Derive the range from the bit count rather than looking it up. The checks call them with 4 and 12 as well as 8, 16, 32 and 64, so a table of remembered constants will not pass.

What your program must do

  • Work each range out on paper from the bit count before writing any code
  • Derive them in code, so 4 bits and 12 bits also come out right
  • Check signed ranges against Byte.MIN_VALUE through Long.MIN_VALUE and their maxima
  • State why countOf cannot return 2^63 or 2^64 in a signed long
  • Say why the top of the range is one smaller than the bottom in size
Ranges.java
public class Ranges {

    // Derive these from the bit count. Do NOT return remembered constants:
    // the checks call these with 4 bits and 12 bits as well.

    // Signed, so one bit is the sign.
    static long minOf(int bits) { return 0; }  // TODO
    static long maxOf(int bits) { return 0; }  // TODO

    // An unsigned n-bit pattern. Java's char is the only unsigned integral
    // primitive, and it represents one UTF-16 code unit.
    static long unsignedMaxOf(int bits) { return 0; }  // TODO

    // How many values fit. The long return type limits this method to 1..62 bits:
    // 2^63 and 2^64 are positive counts that do not fit in a signed long.
    static long countOf(int bits) { return 0; }  // TODO

    public static void main(String[] args) {
        // TODO: print signed ranges for 8, 16, 32 and 64 bits
        // TODO: print counts only where the positive result fits in long
    }
}
Hint 1
n bits hold 2 to the power of n different values. One bit is the sign, so the magnitude has n-1 bits left.
Hint 2
1L << (bits - 1) gives the sign-bit pattern. At 64 bits that pattern is Long.MIN_VALUE; fixed-width overflow also makes subtracting one produce Long.MAX_VALUE. Validate the supported width before shifting because Java masks long shift distances modulo 64.
Hint 3almost the answer
Zero has to live somewhere, and it lives on the positive side. So the positives run 0 to 2^(n-1) - 1 and the negatives run -2^(n-1) to -1, which is why the bottom reaches one further than the top.
What this is really testing

Whether the ranges are numbers you memorised or numbers you can derive. Anyone can look up that int stops at 2147483647. Deriving it from 32 bits makes the value explainable instead of memorised.

08

After the credits

Section 2.2 derives the bit rules behind signed integers and binary floating point. Section 2.3 then follows values as Java widens, narrows, and promotes their types.

The distinction between a UTF-16 code unit and a Unicode code point returns in Phase IX when strings become the main subject.

Threads you opened in this section