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.
Table of Contents
Before you start
You should be clear about:
- The types
int,long,double,char,booleanandString, and when to use each. - That
int / intgivesintand that the cast has to go before dividing. - That
intoverflow gives no warning and that theLgoes 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
- What doesn’t change goes into
finalconstants: price per credit, fee and number of instalments. Every number is written just once. - The sums in
double: credits amount, discount and total. - We work out the discount twice, wrong and right, to watch integer division in action.
- 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:
| Step | Sum | Result |
|---|---|---|
| Credits amount | 24 × 15.50 | 372.00 € |
| Wrong discount | 25 / 100 = 0 → 0 × 372 | 0 € |
| Right discount | 25 / 100.0 = 0.25 → 0.25 × 372 | 93.00 € |
| Total | 372 − 93 + 27.30 | 306.30 € |
| In cents | 306.30 × 100 | 30630 |
| Instalment | 30630 / 4 | 7657 |
| Left over | 30630 % 4 | 2 |
| Last instalment | 7657 + 2 | 7659 |
| Check | 7657 × 3 + 7659 | 30630 |
Three details of the output worth a look:
- The first line says
24 credits x 15.5: the text was built with+, which writes thedoubleJava’s way (point and no trailing zero). The amount on the right goes throughprintfand 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:
-discountWrongis «minus zero», and negative zero exists indouble. It doesn’t change any sum, but on a real receipt it would look odd. instalmentCents / 100.0goes 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
- Change
INSTALMENTSto 3 and then to 7. How many cents are left over each time? Check that the check line still gives 30630. - 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
- The theory average written three ways: with no decimals, with the cast afterwards and dividing by
2.0. - The weighting with
/ 100and with/ 100.0. - Three ways of keeping the final grade:
(int),Math.roundand rounding to one decimal by multiplying and dividing by 10. - All with
printlnto see the number exactly as Java stores it, and a last line withprintfto 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
Line by line, with the sum done by hand:
| Line | Expression | Sum | Prints |
|---|---|---|---|
| Average (int) | (7 + 8) / 2 | 15 / 2 integer | 7 |
| Average (late cast) | (double) ((7 + 8) / 2) | first 15 / 2 = 7, then 7.0 | 7.0 |
| Average (2.0) | (7 + 8) / 2.0 | 15 / 2.0 | 7.5 |
| Final wrong | 60 / 100 * 7.5 + 40 / 100 * 6.5 | 0 × 7.5 + 0 × 6.5 | 0.0 |
| Final right | 60 / 100.0 * 7.5 + 40 / 100.0 * 6.5 | 4.5 + 2.6 | 7.1 |
| Truncated | (int) 7.1 | drops decimals | 7 |
| Rounded | Math.round(7.1) | to the nearest whole | 7 |
| One decimal | Math.round(7.1 * 10) / 10.0 | 71 / 10.0 | 7.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
- Change
practicalto4.95and work out (before running it) what the truncated, rounded and one-decimal grades will be. - 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
- 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'. - The username: a
charplus aStringgives aString.surname1.toLowerCase()puts the surname in small letters. - 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, inint(wrong) and inlong(right). - 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. - The comparison: with
==and withequals().
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
The type trace, which is where the fun of this program lies:
| Expression | Type | Value |
|---|---|---|
name.charAt(0) | char | 'L' (76) |
initial + 32 | int | 108 |
(char) (initial + 32) | char | 'l' |
lowerInitial + surname1.toLowerCase() | String | "lsantana" |
entryYear * 10000000 + studentNumber | int (overflowed) | -1214832159 |
entryYear * 10000000L + studentNumber | long | 20260004321 |
record % 23 | long | 2 |
(int) (record % 23) | int | 2 |
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
- Change
nameto"Pablo"andsurname1to"Medina". What username comes out? And if the name started with Á? - Change
studentNumberto 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 / 100is 0. Divide by100.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.96is a 4. - Working out in
intwhat doesn’t fit: the 11-digit record number needslongand theLin the sum, not just in the variable. - Forgetting that
char + intisint: without the cast,initial + 32is 108, not'l'. ==with texts built at run time: alwaysequals().
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:
- In
int averageInt = sum / 2;you’ll see a 7 appear (not 7.5): the division between twointvalues has already thrown the decimal away. - In
double averageLate = (double) (sum / 2);you get7.0: the decimal is there, but it’s zero. The cast arrived too late. - In
double weightWrong = weight / 100;you get0.0. That variable, multiplied by any grade, is my classmate’s 0.0. - Compare
weightWrongandweightRighton the last line of the output:0.0against0.6, and the only difference in the code is100against100.0.
Summary
- Every piece of data, once: named variables and
finalconstants for what doesn’t change. int / intgivesint. To get decimals, adoubleoperand before dividing (/ 100.0,/ 2.0or a cast in front).(int)truncates;Math.roundrounds and returnslong.- Money, in whole cents:
/shares out and%says what’s left over. - If the sum can go past 2,147,483,647,
longand theLon the first number. char + intgivesint; texts are compared withequals().
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
longand where to put theL? - Do you know why
username == "lsantana"givesfalse? - 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 →




IC2 practical mock exam — flowcharts, fork and threads in C (and the screenshots you can’t leave out)