Java variables practice — Laura’s receipt, final grade and student card (with the grade that came out 0.0)

The week before the first FP2 practical exam, a classmate in my group programmed the subject’s weighted final grade: 60 % theory, 40 % practicals. It compiled first time, he ran it with his grades… and got a 0.0. He checked the grades, checked the percentages, even checked he had the right file open. The bug was in a single expression: 60 / 100. Two int values, integer division: zero. And zero times any grade is zero.

That’s the lesson of this practice: with variables in Java, the program hardly ever complains. It compiles, it runs and it gives you a number. The job is knowing whether that number is the right one.

Here you’ll write three programs about the same student from the syntax articles, Laura (made-up data), and in all three there’s a trap from the Java variables theory waiting: the tuition receipt with a discount and four instalments split in cents, the final FP2 grade with averages and weights, and the student card, with a username built out of char values and a record number that doesn’t fit in an int. All three are medium level and use data that looks like the real thing: percentages stored as integers, cents that don’t split evenly, texts built at run time. And none of them uses Scanner, if or loops yet: the data is written in the program, which is what this topic is about.

The output is real, from Java 21, shown as it looks on a computer set to English: printf uses the decimal separator of your computer’s language, so on a Spanish one you’d see commas.

Before you start

You should be clear about:

  • The types int, long, double, char, boolean and String, and when to use each.
  • That int / int gives int and that the cast has to go before dividing.
  • That int overflow gives no warning and that the L goes on the first number of the sum.
  • That texts are compared with equals(), not with ==.

If any of these sounds shaky, go over the theory before carrying on.

Each program is a class with main in its own file, named after the class (Tuition.java, FinalGrade.java, StudentCard.java). Compile them with javac Tuition.java and run them with java Tuition, or paste them into the Java playground on this site. On a computer set to Spanish, printf writes decimals with a comma: that’s normal.

Program 1: the tuition receipt with no repeated data

Laura has 24 credits at 15.50 €, a 25 % discount on the credits (not on the fee) and an admin fee of 27.30 €. She wants to pay in four instalments. The program has to work out the receipt and split the total into instalments without losing a single cent. What makes it tricky: the percentage comes as a whole number (25) and 306.30 € doesn’t divide evenly by four.

The idea

  1. What doesn’t change goes into final constants: price per credit, fee and number of instalments. Every number is written just once.
  2. The sums in double: credits amount, discount and total.
  3. We work out the discount twice, wrong and right, to watch integer division in action.
  4. For the instalments we turn the total into whole cents (int): that way / shares out and % says how many cents are left over, with no decimals going missing on the way. The last instalment keeps what’s left over.

Before programming, think about the type of every intermediate result. discountPercent / 100 mixes two int values: what does 25 / 100 give?

The code

/**
 * Program 1: Laura's tuition receipt, with every piece of data in its own variable.
 * Made-up data: 24 credits at 15.50 €, an admin fee of 27.30 €, a 25 % discount and 4 instalments.
 */
public class Tuition {
    public static void main(String[] args) {
        // Data that doesn't change in the whole program: constants
        final double CREDIT_PRICE = 15.50;
        final double ADMIN_FEE = 27.30;
        final int INSTALMENTS = 4;

        // Laura's data
        String student = "Laura Santana Ruiz";
        int credits = 24;
        int discountPercent = 25;                               // 25 %, stored as an integer

        // Sums
        double creditsAmount = credits * CREDIT_PRICE;          // int * double = double
        double discountWrong = discountPercent / 100 * creditsAmount;   // 25 / 100 = 0 (int)
        double discountRight = discountPercent / 100.0 * creditsAmount; // 25 / 100.0 = 0.25
        double total = creditsAmount - discountRight + ADMIN_FEE;

        // Instalments in cents: exact whole numbers, no decimals to lose
        int totalCents = (int) Math.round(total * 100);         // 306.30 € → 30630
        int instalmentCents = totalCents / INSTALMENTS;         // integer division
        int leftOver = totalCents % INSTALMENTS;                // cents it cannot share out
        int lastInstalment = instalmentCents + leftOver;        // the last one pays what is left over

        System.out.println("TUITION RECEIPT");
        System.out.println("Student: " + student);
        System.out.printf("%-26s %8.2f €%n", credits + " credits x " + CREDIT_PRICE, creditsAmount);
        System.out.printf("%-26s %8.2f €%n", "Discount (int / 100)", -discountWrong);
        System.out.printf("%-26s %8.2f €%n", "Discount (int / 100.0)", -discountRight);
        System.out.printf("%-26s %8.2f €%n", "Admin fee", ADMIN_FEE);
        System.out.printf("%-26s %8.2f €%n", "TOTAL", total);
        System.out.println("In cents: " + totalCents);
        System.out.printf("%d instalments of %.2f € and the last one of %.2f € (%d cents left over)%n",
                INSTALMENTS - 1, instalmentCents / 100.0, lastInstalment / 100.0, leftOver);
        System.out.println("Check: " + (instalmentCents * (INSTALMENTS - 1) + lastInstalment) + " cents");
    }
}

The instalments part, close up:

// Instalments in cents: exact whole numbers, no decimals to lose
int totalCents = (int) Math.round(total * 100);          // 306.30 € → 30630
int instalmentCents = totalCents / INSTALMENTS;          // integer division
int leftOver = totalCents % INSTALMENTS;                 // cents it cannot share out
int lastInstalment = instalmentCents + leftOver;         // the last one pays what is left over

Math.round(total * 100) turns 306.30 into 30630 cents. Why Math.round and not a cast? Because total * 100 is a double and could come out as something like 30629.999999… because of the way decimals are stored; the cast would leave 30629 and you’d lose a cent. Math.round rounds to the nearest whole number and returns a long; the (int) in front brings it down to int because we know it fits.

Output

TUITION RECEIPT
Student: Laura Santana Ruiz
24 credits x 15.5            372.00 €
Discount (int / 100)          -0.00 €
Discount (int / 100.0)       -93.00 €
Admin fee                     27.30 €
TOTAL                        306.30 €
In cents: 30630
3 instalments of 76.57 € and the last one of 76.59 € (2 cents left over)
Check: 30630 cents

Let’s check it by hand:

StepSumResult
Credits amount24 × 15.50372.00 €
Wrong discount25 / 100 = 0 → 0 × 3720 €
Right discount25 / 100.0 = 0.25 → 0.25 × 37293.00 €
Total372 − 93 + 27.30306.30 €
In cents306.30 × 10030630
Instalment30630 / 47657
Left over30630 % 42
Last instalment7657 + 27659
Check7657 × 3 + 765930630
The four tuition instalments306.30 euros are 30630 cents. 30630 / 4 gives 7657 cents per instalment and 30630 % 4 gives 2 cents left over; the last instalment is 7659. 7657 times 3 plus 7659 adds up to 30630.306.30 € → 30630cents (int)30630 / 4 = 765730630 % 4 = 2Instalment 17657Instalment 27657Instalment 37657Instalment 476597657 + 2 left over7657 × 3 + 7659 = 30630Not a cent is lost: / shares out and % tells you what is left
With whole numbers, / and % work together: one shares out and the other keeps what could not be shared.

Three details of the output worth a look:

  • The first line says 24 credits x 15.5: the text was built with +, which writes the double Java’s way (point and no trailing zero). The amount on the right goes through printf and gets two decimals. It’s the difference between concatenating and formatting that you saw in syntax.
  • The wrong discount shows a minus sign in front of zero: -discountWrong is «minus zero», and negative zero exists in double. It doesn’t change any sum, but on a real receipt it would look odd.
  • instalmentCents / 100.0 goes back to euros for printing: with / 100 (integer) you’d get exactly 76 €.

Questions you might ask

Why not store the percentage directly as 0.25? You could, and you’d avoid the trap. But in real programs percentages often arrive as whole numbers (from a form, a database, a task that says «25 %»), and you need to know how to convert them without losing anything.

Why split in cents instead of dividing 306.30 by 4? Because 306.30 / 4 = 76.575, which isn’t an amount you can charge. You’d have to round each instalment, and four rounded instalments don’t have to add up to the total. In whole cents, / and % keep it balanced by construction.

Why the last instalment and not the first? It’s a design decision: many bills put the adjustment in the last payment. If you wanted it in the first, you’d swap lastInstalment for a first instalment and change the text. The sum is the same.

What would happen with int totalCents = total * 100;? It wouldn’t compile: double to int is narrowing and javac asks for the cast (possible lossy conversion). And with the cast but without Math.round, you risk losing that cent we talked about.

Do I need final if nobody is going to change those variables? Not for it to work, but final makes it clear they are constants and the compiler protects you if one day someone (you, at three in the morning) tries to reassign them.

Writing the percentage as discount / 100 * amount with a whole-number discount. Integer division leaves it at 0 and the program works out a zero-euro discount without any warning. Write / 100.0.

For money, cents in an int or a long. Adding, sharing out and comparing whole numbers is exact; with double you drag along small errors that, sometimes, show.

Try it yourself

  1. Change INSTALMENTS to 3 and then to 7. How many cents are left over each time? Check that the check line still gives 30630.
  2. Make the discount 12 % and apply it to the fee as well. How much does Laura pay now? Work it out by hand before running it.

Program 2: the final FP2 grade

Now my classmate’s story, but with Laura’s grades: two whole-number midterms (7 and 8) that make the theory average, a practical marked in half points (6.5) and weights of 60 % and 40 % stored as integers. The program works out the average and the final grade in several ways to see which ones fail, and then decides what goes on the record: truncate, round or one decimal.

The idea

  1. The theory average written three ways: with no decimals, with the cast afterwards and dividing by 2.0.
  2. The weighting with / 100 and with / 100.0.
  3. Three ways of keeping the final grade: (int), Math.round and rounding to one decimal by multiplying and dividing by 10.
  4. All with println to see the number exactly as Java stores it, and a last line with printf to compare.

Before running it, write down on paper what you think each line will print. The ones you get wrong are the ones you need to understand.

The code

/**
 * Program 2: Laura's final FP2 grade (made-up weights and grades).
 * Midterms 1 and 2 make the theory average (60 %) and the practical weighs 40 %.
 */
public class FinalGrade {
    public static void main(String[] args) {
        int midterm1 = 7;                     // the midterm grades are whole numbers
        int midterm2 = 8;
        double practical = 6.5;               // the practical is marked in half points
        int theoryWeight = 60;                // percentages stored as int
        int practicalWeight = 40;

        // 1. The theory average: three ways to write it
        int averageInt = (midterm1 + midterm2) / 2;                      // 15 / 2 = 7
        double averageLate = (double) ((midterm1 + midterm2) / 2);       // the cast arrives too late: 7.0
        double theoryAverage = (midterm1 + midterm2) / 2.0;              // 15 / 2.0 = 7.5

        // 2. The weighting: the same mistake, now with the percentages
        double finalWrong = theoryWeight / 100 * theoryAverage + practicalWeight / 100 * practical;
        double finalRight = theoryWeight / 100.0 * theoryAverage + practicalWeight / 100.0 * practical;

        // 3. What goes on the official record
        int truncated = (int) finalRight;              // drops the decimals
        long rounded = Math.round(finalRight);         // rounds (returns long)
        double oneDecimal = Math.round(finalRight * 10) / 10.0;   // round to one decimal

        System.out.println("Theory average (int):          " + averageInt);
        System.out.println("Theory average (late cast):    " + averageLate);
        System.out.println("Theory average (2.0):          " + theoryAverage);
        System.out.println("Final grade (int / 100):       " + finalWrong);
        System.out.println("Final grade (int / 100.0):     " + finalRight);
        System.out.println("Truncated with (int):          " + truncated);
        System.out.println("Rounded with Math.round:       " + rounded);
        System.out.println("To one decimal:                " + oneDecimal);
        System.out.printf("With printf and %%.2f:          %.2f%n", finalRight);
    }
}

Output

Theory average (int):          7
Theory average (late cast):    7.0
Theory average (2.0):          7.5
Final grade (int / 100):       0.0
Final grade (int / 100.0):     7.1
Truncated with (int):          7
Rounded with Math.round:       7
To one decimal:                7.1
With printf and %.2f:          7.10
The same trap in the average and in the weightsAverage: (7 + 8) / 2 gives 7, the cast afterwards gives 7.0 and dividing by 2.0 gives 7.5. Weighting: 60 / 100 and 40 / 100 give 0, so the grade comes out 0.0; with 100.0 it is 4.5 + 2.6 = 7.1.The theory averageThe 60 / 40 weighting(7 + 8) / 2→ 15 / 2 = 7(double) ((7 + 8) / 2)→ (double) 7 = 7.0(7 + 8) / 2.0→ 15 / 2.0 = 7.560 / 100 * 7.5→ 0 * 7.5 = 0.040 / 100 * 6.5→ 0 * 6.5 = 0.060 / 100.0 * 7.5 + 40 / 100.0 * 6.5→ 4.5 + 2.6 = 7.1int / int gives int, and a cast or a double on the left arrives too late: the decimal has to be inside
Both columns fail for the same reason: a division between two ints before any decimal shows up.

Line by line, with the sum done by hand:

LineExpressionSumPrints
Average (int)(7 + 8) / 215 / 2 integer7
Average (late cast)(double) ((7 + 8) / 2)first 15 / 2 = 7, then 7.07.0
Average (2.0)(7 + 8) / 2.015 / 2.07.5
Final wrong60 / 100 * 7.5 + 40 / 100 * 6.50 × 7.5 + 0 × 6.50.0
Final right60 / 100.0 * 7.5 + 40 / 100.0 * 6.54.5 + 2.67.1
Truncated(int) 7.1drops decimals7
RoundedMath.round(7.1)to the nearest whole7
One decimalMath.round(7.1 * 10) / 10.071 / 10.07.1

The 0.0 line is my classmate’s grade. Notice that both halves of the sum fail at once: 60 / 100 and 40 / 100 are zero, so it doesn’t matter how good the grades are.

Questions you might ask

Why does Math.round(finalRight * 10) / 10.0 round to one decimal? Multiplying by 10 moves the decimal point one place (7.1 → 71.0), Math.round rounds to a whole number (71) and dividing by 10.0 puts it back (7.1). With / 10 (integer) you’d lose the decimal you wanted to keep: 71 / 10 = 7.

Truncate or round? It depends on the subject’s rules. With 7.1 it makes no difference, but with 4.96 truncating gives a 4 (fail) and rounding gives a 5. That’s why it matters to know that (int) does not round.

Why long rounded and not int? Because Math.round with a double returns long. If you want an int, (int) Math.round(finalRight), as in program 1.

And if the midterm grades had decimals too? Then they’d be double and (midterm1 + midterm2) / 2 would already be right, because one of the operands of the division would be a decimal. The trap only appears when everything to the left of the division is an integer.

Why does the last line say 7.10 and the one above 7.1? printf with %.2f always writes two decimals, with the separator of the computer’s language (on a Spanish computer, 7.10). println writes 7.1, Java’s way. To show grades to a person, printf.

Writing the weights as weight / 100 with a whole-number weight (or the percentage of program 1, which is the same thing). Every time you see a division, ask yourself what type both sides are.

Try it yourself

  1. Change practical to 4.95 and work out (before running it) what the truncated, rounded and one-decimal grades will be.
  2. Add a third midterm with a grade of 9 and work out the theory average of the three without losing the decimals.

Program 3: the university student card

The last program generates the data for Laura’s card: her university username (first-name initial plus first surname, in small letters), her email, the record number (year of entry followed by the student number: 20260004321) and a check letter worked out like the one on the Spanish ID card. At the end it checks whether the username matches the one the admin office has. What makes it tricky: the initial has to be turned into a small letter with char arithmetic, the record number has eleven digits and comparing texts has its trap.

The idea

  1. The initial: charAt(0) gives 'L'. In ASCII, the distance between capital and small letters is 32 ('a' − 'A' = 97 − 65), so (char) (initial + 32) gives 'l'.
  2. The username: a char plus a String gives a String. surname1.toLowerCase() puts the surname in small letters.
  3. The record number: year × 10,000,000 + student number. 2026 × 10,000,000 = 20,260,000,000, which doesn’t fit in an int: we work it out twice, in int (wrong) and in long (right).
  4. The letter: the remainder of dividing the record number by 23 picks a letter from the string TRWAGMYFPDXBNJZSQVHLCKE, the same one the Spanish ID card uses.
  5. The comparison: with == and with equals().

Before programming, think about the type of initial + 32. Is it a char?

The code

/**
 * Program 3: Laura's university card (made-up data).
 * Username built from char and String, a record number that doesn't fit in an int and comparing texts.
 */
public class StudentCard {
    public static void main(String[] args) {
        String name = "Laura";
        String surname1 = "Santana";
        int entryYear = 2026;
        int studentNumber = 4321;

        // The username: first-name initial + first surname, in small letters
        char initial = name.charAt(0);                         // 'L'
        char lowerInitial = (char) (initial + 32);             // in ASCII, 'a' - 'A' = 32
        String username = lowerInitial + surname1.toLowerCase();   // char + String = String
        System.out.println("Username:    " + username);
        System.out.println("Email:       " + username + "@alu.ulpgc.es");

        // The record number: year * 10000000 + student number = 20260004321 (11 digits)
        int recordWrong = entryYear * 10000000 + studentNumber;     // goes past 2147483647
        long record = entryYear * 10000000L + studentNumber;        // 10000000L: works it out in long
        System.out.println("Record (int):      " + recordWrong);
        System.out.println("Record (long):     " + record);

        // The check letter: the remainder of dividing by 23, like the Spanish ID card
        String letters = "TRWAGMYFPDXBNJZSQVHLCKE";
        int remainder = (int) (record % 23);                   // long % int = long → cast to int
        char check = letters.charAt(remainder);
        System.out.println("Remainder of 23: " + remainder + " → letter " + check);
        System.out.println("Card:        " + record + "-" + check);

        // Is it the username the admin office has?
        String officeUsername = "lsantana";
        System.out.println("username == \"lsantana\"      " + (username == officeUsername));
        System.out.println("username.equals(\"lsantana\") " + username.equals(officeUsername));
    }
}

Output

Username:    lsantana
Email:       lsantana@alu.ulpgc.es
Record (int):      -1214832159
Record (long):     20260004321
Remainder of 23: 2 → letter W
Card:        20260004321-W
username == "lsantana"      false
username.equals("lsantana") true
From the name to the username lsantanacharAt(0) gives 'L', which is 76; adding 32 gives the int 108; the cast turns it into 'l'; a char plus a String gives the String lsantana. As it is built at run time, == with the literal gives false and equals gives true.name.charAt(0)'L'76initial + 32108int(char) (initial + 32)'l'charlowerInitial + "santana""lsantana"StringIn ASCII, 'a' – 'A' = 32: adding it turns a capital into a small letterusername is built at run time → new objectusername == "lsantana" → false username.equals("lsantana") → true
Each step changes the type: char, int, char and String. Look at the last box: the text is the same, the object is not.

The type trace, which is where the fun of this program lies:

ExpressionTypeValue
name.charAt(0)char'L' (76)
initial + 32int108
(char) (initial + 32)char'l'
lowerInitial + surname1.toLowerCase()String"lsantana"
entryYear * 10000000 + studentNumberint (overflowed)-1214832159
entryYear * 10000000L + studentNumberlong20260004321
record % 23long2
(int) (record % 23)int2
letters.charAt(2)char'W'

Checking the remainder: 23 × 880869753 = 20260004319, and 20260004321 − 20260004319 = 2. The letter in position 2 (counting from 0: T, R, W) is W.

And the last pair of lines is the theory come true: username holds the text lsantana, but it was built at run time (from variables), so it’s a new object and == with the literal gives false. equals() compares the letters and gives true.

Questions you might ask

Why initial + 32 and not Character.toLowerCase(initial)? In a real program you’d use Character.toLowerCase, which works with any letter (including Ñ). Here we do it with addition to see that a char is a number. It works with the letters A to Z.

Why does the int record number come out negative? 20,260,004,321 goes past the maximum of an int (2,147,483,647) and wraps round several times. The result is rubbish shaped like a number. With no error at all.

Why (int) (record % 23)? record is long, so the remainder is also long, even if it’s worth 2. charAt wants an int, and going from long to int is narrowing: the cast is needed. We know it fits because a remainder of dividing by 23 is always between 0 and 22.

And if I write "l" + "santana" instead of building it with variables? Then == would give true, because javac would join the two literals when compiling (you saw it in the theory). That’s exactly what makes == dangerous: it works in the quick test and fails in the real program.

Why store the record number as a number and not as a String? To be able to work out the letter with %. If you were only going to display it, a String would do (and you’d have no size problems).

Checking the username or the password with ==. In the lab, with texts coming from the keyboard, it will always give false. equals().

The check letter of this record number is made up to practice %; universities don’t work it out this way.

Try it yourself

  1. Change name to "Pablo" and surname1 to "Medina". What username comes out? And if the name started with Á?
  2. Change studentNumber to 4322 and work out the letter by hand before running it (hint: the remainder goes up one at a time).

Common mistakes in this practice

  • Whole-number percentages divided by 100: 25 / 100 is 0. Divide by 100.0.
  • The cast after the division: (double) ((a + b) / 2) arrives too late. The cast goes in front of what is divided.
  • Cast instead of Math.round: (int) (total * 100) can lose a cent; (int) 4.96 is a 4.
  • Working out in int what doesn’t fit: the 11-digit record number needs long and the L in the sum, not just in the variable.
  • Forgetting that char + int is int: without the cast, initial + 32 is 108, not 'l'.
  • == with texts built at run time: always equals().

Watch it step by step in Python Tutor

pythontutor.com runs the code line by line and shows the value and type of every variable. Choose Java and paste this cut-down version of program 2:

public class AverageTutor {
    public static void main(String[] args) {
        int midterm1 = 7;
        int midterm2 = 8;
        int sum = midterm1 + midterm2;
        int averageInt = sum / 2;
        double averageLate = (double) (sum / 2);
        double averageRight = sum / 2.0;
        int weight = 60;
        double weightWrong = weight / 100;
        double weightRight = weight / 100.0;
        System.out.println(averageInt + " " + averageLate + " " + averageRight);
        System.out.println(weightWrong + " " + weightRight);
    }
}

Output:

7 7.0 7.5
0.0 0.6

What to watch:

  1. In int averageInt = sum / 2; you’ll see a 7 appear (not 7.5): the division between two int values has already thrown the decimal away.
  2. In double averageLate = (double) (sum / 2); you get 7.0: the decimal is there, but it’s zero. The cast arrived too late.
  3. In double weightWrong = weight / 100; you get 0.0. That variable, multiplied by any grade, is my classmate’s 0.0.
  4. Compare weightWrong and weightRight on the last line of the output: 0.0 against 0.6, and the only difference in the code is 100 against 100.0.

Summary

  • Every piece of data, once: named variables and final constants for what doesn’t change.
  • int / int gives int. To get decimals, a double operand before dividing (/ 100.0, / 2.0 or a cast in front).
  • (int) truncates; Math.round rounds and returns long.
  • Money, in whole cents: / shares out and % says what’s left over.
  • If the sum can go past 2,147,483,647, long and the L on the first number.
  • char + int gives int; texts are compared with equals().

Checklist: have you practised it?

  • Have you run the three programs and checked the output?
  • Could you explain why the wrong discount and the wrong final grade come out as zero?
  • Do you know how to split an amount into instalments with / and % without losing cents?
  • Do you know when a sum needs long and where to put the L?
  • Do you know why username == "lsantana" gives false?
  • Have you tried the «Try it yourself» tasks without looking at the theory?

If you answered yes to almost everything, you’re ready for the Java variables exercises, where I no longer give you the code. See you there!

Next step: exercisesJava variables exercises — BMI, seconds and the print shop inventory (and the percentage that got stuck at 100)

Did this article help you? Here is how you can support SergioLearns →