Operators are the verbs of the language: what turns inert data into calculations. You have already used + and - intuitively, but Java has a broad catalogue and several traps that catch everyone at least once: integer division discarding the decimals, the difference between i++ and ++i, string comparison with == that sometimes works and sometimes does not, and the treacherous precision of double when money is involved. This lesson goes through them all calmly, always on the same BiblioTech case: calculating the late fine for a loan from the Nexus Software library.

Contents

  1. Arithmetic operators and the integer-division trap
  2. The modulo operator
  3. Compound assignment operators
  4. Increment and decrement: pre and post
  5. Relational operators
  6. Logical operators and short-circuit evaluation
  7. The ternary operator
  8. Bitwise and shift operators
  9. The instanceof operator
  10. Precedence and associativity
  11. Comparing strings: == versus equals
  12. The precision of double and why BigDecimal exists
  13. Common Mistakes and Tips
  14. Exercises

  1. Arithmetic operators and the integer-division trap

The five basic arithmetic operators:

Operator Name Example Result
+ Addition 12 + 3 15
- Subtraction 27 - 15 12
* Multiplication 12 * 2 24
/ Division 12 / 5 2 (careful!)
% Modulo (remainder) 12 % 5 2

The 5 / 2 trap

It is the number one mistake of anyone starting out in Java:

int total = 5;
int parts = 2;
System.out.println(total / parts);      // prints 2, not 2.5

The rule is this: if both operands are integers, the division is an integer division and the result is truncated towards zero, discarding the decimals. Java does not round, it cuts.

And this is not fixed by storing the result in a double:

double result = total / parts;
System.out.println(result);             // 2.0, not 2.5

Why? Because the operation total / parts is evaluated first, between integers, giving 2; only afterwards is that 2 converted to a double. The conversion arrives too late.

The three correct solutions:

// 1. Cast one of the operands BEFORE dividing
double result = (double) total / parts;            // 2.5

// 2. Declare the variables as double from the start
double totalD = 5;
double partsD = 2;
double result2 = totalD / partsD;                  // 2.5

// 3. Multiply by 1.0 (a less readable trick, but you will see it in real code)
double result3 = total * 1.0 / parts;              // 2.5

Watch the order in the cast: (double)(total / parts) does not work. The parentheses make the integer division happen first and the conversion afterwards: it gives 2.0. The cast has to be applied to one of the operands.

Division by zero

It depends on the type, and the difference matters:

int a = 5 / 0;         // ArithmeticException: / by zero -> the program stops
double b = 5.0 / 0;    // Infinity  -> the program CONTINUES
double c = 0.0 / 0;    // NaN (Not a Number) -> the program CONTINUES

Integer division by zero throws a runtime error. Decimal division produces special values (Infinity, NaN) that propagate silently through every later calculation, which can be worse: you end up printing NaN EUR on a receipt without ever having seen an error.

Applied to BiblioTech

final int LOAN_DAYS = 15;

int elapsedDays = 27;
int daysLate = elapsedDays - LOAN_DAYS;                // 12 days late

final double DAILY_RATE = 0.25;
double fine = daysLate * DAILY_RATE;                   // 12 * 0.25 = 3.0

System.out.println("Days late: " + daysLate);
System.out.println("Fine: " + fine + " EUR");

Here daysLate is an int and DAILY_RATE is a double: Java promotes the integer to a decimal before multiplying and the result is correct. Automatic promotion saves us; division is the only one to watch out for.

  1. The modulo operator

% returns the remainder of an integer division. It is far more useful than it looks at first sight.

System.out.println(17 % 5);     // 2  (17 = 5*3 + 2)
System.out.println(12 % 4);     // 0  (exact division)
System.out.println(3 % 5);      // 3  (3 = 5*0 + 3)
System.out.println(-7 % 3);     // -1 (in Java the sign comes from the DIVIDEND)
System.out.println(7.5 % 2);    // 1.5 (it also works with decimals)

Common uses:

Goal Expression Explanation
Is it even? n % 2 == 0 Remainder zero when divided by 2
Is it a multiple of 7? n % 7 == 0 Generalisation of the previous one
Last digit n % 10 Remainder of dividing by 10
Cycle within a range 0..n−1 i % n It never leaves the range
Break down time minutes % 60 Minutes left over from the whole hours

Applied to BiblioTech, to express the delay in weeks and leftover days:

int daysLate = 27;

int weeksLate = daysLate / 7;        // integer division: 3 weeks
int leftoverDays = daysLate % 7;     // remainder: 6 days

System.out.println("Late by: " + weeksLate + " weeks and " + leftoverDays + " days");
// Late by: 3 weeks and 6 days

Notice how / and % work as a pair: one gives the quotient, the other what is left over. It is a pattern that will come back many times.

  1. Compound assignment operators

They are shortcuts for operating on a variable and storing the result back in it:

Operator Equivalent to Example
+= x = x + y fine += 0.25;
-= x = x - y daysLate -= 3;
*= x = x * y fine *= 2;
/= x = x / y total /= 4;
%= x = x % y counter %= 7;
double fine = 3.0;
fine += 1.50;      // fine is now 4.5
fine *= 2;         // fine is now 9.0

There is a little-known detail: compound assignment includes an implicit cast. This compiles:

int daysLate = 12;
daysLate += 0.75;                 // compiles: equivalent to daysLate = (int)(daysLate + 0.75)
System.out.println(daysLate);     // 12  <- the 0.75 is lost in the truncation

whereas the equivalent "long" form does not compile:

daysLate = daysLate + 0.75;       // ERROR: incompatible types: possible lossy conversion

It is an asymmetry in the language that can hide precision loss with no warning at all. It is worth knowing about.

  1. Increment and decrement: pre and post

++ adds 1 and -- subtracts 1. What is interesting is that they can be written before or after the variable, and that changes the value of the expression:

  • Post-increment x++: uses the current value, and then increments.
  • Pre-increment ++x: increments first, and then uses the new value.
int a = 5;
int resultA = a++;        // resultA receives 5, and then a becomes 6
System.out.println("a = " + a + ", resultA = " + resultA);
// a = 6, resultA = 5

int b = 5;
int resultB = ++b;        // b becomes 6 first, and resultB receives 6
System.out.println("b = " + b + ", resultB = " + resultB);
// b = 6, resultB = 6

The table sums up the difference, always starting from x = 5:

Expression Value of the expression Final value of x
x++ 5 6
++x 6 6
x-- 5 4
--x 4 4

When it really matters: if the ++ is alone on its own line (counter++;), pre and post are identical and it makes no difference which you use. The difference only shows up when the value of the expression is used for something, such as an assignment, an index or an argument.

Practical advice: do not write expressions that depend on that subtlety. Code like int x = i++ + ++i; is a riddle, not a program. When it matters, split it into two clear lines.

  1. Relational operators

They compare two values and always produce a boolean:

Operator Meaning Example Result
== Equal to daysLate == 0 false if there is a delay
!= Not equal to daysLate != 0 true if there is a delay
> Greater than 27 > 15 true
< Less than 12 < 15 true
>= Greater than or equal 15 >= 15 true
<= Less than or equal 20 <= 15 false
final int LOAN_DAYS = 15;
int elapsedDays = 27;

boolean isLate = elapsedDays > LOAN_DAYS;
System.out.println("Is late: " + isLate);            // true

It repeats the idea from the previous lesson: a comparison is an expression that is worth true or false on its own. You do not need an if to obtain it; the if (module 2) is for deciding what to do with it.

Two warnings:

  • == compares values for primitives, but references for objects. That is the topic of section 11.
  • The most expensive typo in programming is writing = (assignment) where you meant == (comparison). In Java, fortunately, if (x = 5) does not compile unless x is a boolean, because an int assignment does not produce a boolean. The compiler protects you.

  1. Logical operators and short-circuit evaluation

They combine boolean values:

Operator Name Result
&& Logical AND true only if both are true
|| Logical OR true if at least one is true
! Negation (NOT) Inverts the value

Truth tables:

a b a && b a || b !a
true true true true false
true false false true false
false true false true true
false false false false true

Applied to BiblioTech:

boolean available = false;
int daysLate = 12;
boolean isActiveEmployee = true;

boolean onLoanAndLate = !available && daysLate > 0;          // true
boolean needsNotice = daysLate > 0 || !available;            // true
boolean canReserve = available && isActiveEmployee;          // false

Short-circuit evaluation

This is the important part. && and || are lazy: they stop evaluating as soon as the result is already decided.

  • In a && b, if a is false, the result is false whatever happens with b. b is not evaluated.
  • In a || b, if a is true, the result is true whatever happens with b. b is not evaluated.
flowchart TD
    A["evaluate a"] --> B{"which operator?"}
    B -- "logical AND" --> C{"is a false?"}
    C -- "Yes" --> D["result false<br/><b>b is NOT evaluated</b>"]
    C -- "No" --> E["evaluate b<br/>result = b"]
    B -- "logical OR" --> F{"is a true?"}
    F -- "Yes" --> G["result true<br/><b>b is NOT evaluated</b>"]
    F -- "No" --> H["evaluate b<br/>result = b"]

Why does it matter? Because it lets you protect dangerous operations by putting the safety check first:

String employee = null;

// This FAILS with NullPointerException: it tries to call length() on null
boolean valid = employee.length() > 0 && employee != null;

// This WORKS: if employee is null, the first condition is false
// and the second one is not even evaluated
boolean valid = employee != null && employee.length() > 0;

The order of the conditions is not cosmetic: it is functional. Remember it, because it is a pattern you will use constantly from module 6 onwards.

The non-short-circuit versions: & and |

There are & and | applied to booleans, which always evaluate both sides:

boolean r1 = false && methodThatPrints();   // the method does NOT run
boolean r2 = false &  methodThatPrints();   // the method DOES run

In practice && and || are used almost always. Remember that & and | also have a completely different meaning on numbers, which is the next section.

  1. The ternary operator

It is Java's only operator with three operands. Its form is:

condition ? valueIfTrue : valueIfFalse

It reads: "if the condition is true, the result is the first thing; otherwise, the second".

int elapsedDays = 27;
final int LOAN_DAYS = 15;

int daysLate = elapsedDays > LOAN_DAYS
        ? elapsedDays - LOAN_DAYS
        : 0;

System.out.println("Days late: " + daysLate);            // 12

This line does something very valuable for us: it stops the delay from being negative. If the book is returned after 10 days, 10 - 15 would give -5, and a negative fine would be absurd (the library paying the employee!). The ternary lets us fix it without using if, which we have not seen yet.

The most common use is adapting a piece of text:

double fine = daysLate * 0.25;
String status = daysLate > 0 ? "OVERDUE" : "ON TIME";
String currency = fine == 1.0 ? "euro" : "euros";

System.out.println("Loan status: " + status);
System.out.println("Amount: " + fine + " " + currency);

They can be nested, but readability degrades very quickly:

// Readable, only just
String category = daysLate == 0 ? "ON TIME"
                : daysLate <= 7 ? "MINOR"
                : "SEVERE";

Practical rule: the ternary is excellent for choosing a value; as soon as you want to run several different actions, you need an if, which arrives in lesson 02-01.

  1. Bitwise and shift operators

These operators work on the binary representation of integers. At first they look esoteric, but they show up in permissions, flags, cryptography, image processing and network protocols, and in technical interviews.

A reminder: an int is 32 bits. The number 12 is 00000000 00000000 00000000 00001100, and 10 is ...00001010.

The bitwise operators

Operator Name Rule per bit 12 op 10 Result
& AND 1 only if both are 1 1100 & 1010 1000 = 8
| OR 1 if either is 1 1100 | 1010 1110 = 14
^ XOR 1 if they are different 1100 ^ 1010 0110 = 6
~ NOT Inverts every bit ~12 -13
int a = 12;   // 1100
int b = 10;   // 1010

System.out.println(a & b);    // 8   -> 1000
System.out.println(a | b);    // 14  -> 1110
System.out.println(a ^ b);    // 6   -> 0110
System.out.println(~a);       // -13

// Seeing the binary representation is the best way to understand it
System.out.println(Integer.toBinaryString(a));       // 1100
System.out.println(Integer.toBinaryString(a & b));   // 1000

The result ~12 = -13 is surprising. The reason is that Java represents negative integers in two's complement, where ~x is always equivalent to -x - 1.

The shift operators

Operator Name What it does Example Result
<< Left shift Moves the bits left, fills with zeros 12 << 2 48
>> Signed right shift Moves right, preserves the sign 12 >> 2 3
>>> Unsigned right shift Moves right, fills with zeros -12 >>> 28 15

The relationship with arithmetic is direct and very easy to remember:

  • x << n is equivalent to multiplying by 2ⁿ
  • x >> n is equivalent to dividing by 2ⁿ (integer division)
System.out.println(12 << 1);    // 24   (12 * 2)
System.out.println(12 << 2);    // 48   (12 * 4)
System.out.println(12 >> 1);    // 6    (12 / 2)
System.out.println(12 >> 2);    // 3    (12 / 4)
System.out.println(-12 >> 2);   // -3   preserves the sign
System.out.println(-12 >>> 2);  // 1073741821  fills with zeros: the sign is lost

A realistic use: permission flags

Where these operators really shine is packing several boolean options into a single integer. In BiblioTech we could encode an employee's permissions over the catalog:

// Each permission occupies a different bit
final int PERMISSION_VIEW     = 1;  // 0001
final int PERMISSION_LEND     = 2;  // 0010
final int PERMISSION_RESERVE  = 4;  // 0100
final int PERMISSION_ADMIN    = 8;  // 1000

// Marta Ruiz can view, borrow and reserve
int martaPermissions = PERMISSION_VIEW | PERMISSION_LEND | PERMISSION_RESERVE;   // 0111 = 7

// Checking one specific permission: AND with the flag
boolean canReserve = (martaPermissions & PERMISSION_RESERVE) != 0;      // true
boolean canAdmin   = (martaPermissions & PERMISSION_ADMIN) != 0;        // false

System.out.println("Marta Ruiz's permissions: " + martaPermissions);
System.out.println("Can reserve: " + canReserve);
System.out.println("Can administer: " + canAdmin);

Four booleans in a single int, with instant checks. It is the mechanism behind Unix file permissions and a great many APIs.

  1. The instanceof operator

instanceof checks whether an object is of a given type and returns a boolean:

String title = "Effective Java";
boolean isText = title instanceof String;   // true

It is mentioned here only to complete the catalogue of operators. Its real usefulness appears when there are type hierarchies and polymorphism, that is, from module 3 onwards. Since Java 16 it also supports pattern matching (if (obj instanceof String s)), which is studied in module 10.

  1. Precedence and associativity

When an expression combines several operators, Java applies a fixed order. From highest to lowest priority:

Level Operators Associativity
1 () [] . Left to right
2 ++ -- (postfix) Left to right
3 ++ -- (prefix), + - (unary), !, ~, casts Right to left
4 * / % Left to right
5 + - Left to right
6 << >> >>> Left to right
7 < <= > >= instanceof Left to right
8 == != Left to right
9 & Left to right
10 ^ Left to right
11 | Left to right
12 && Left to right
13 || Left to right
14 ? : (ternary) Right to left
15 = += -= *= /= %= Right to left

Associativity decides what happens when there are operators of the same level: 10 - 4 - 3 groups as (10 - 4) - 3 = 3, not as 10 - (4 - 3) = 9.

Examples where precedence changes the result:

System.out.println(2 + 3 * 4);        // 14, not 20: * comes before +
System.out.println((2 + 3) * 4);      // 20

System.out.println(10 - 4 - 3);       // 3: left-associative

// Classic trap: & has LOWER priority than !=
// (a & b) != 0  is the correct form; a & b != 0 groups as a & (b != 0) and does not even compile

And a real BiblioTech case where parentheses are missing:

int elapsedDays = 27;
final int LOAN_DAYS = 15;
final double DAILY_RATE = 0.25;

// WRONG: it evaluates (elapsedDays) - (LOAN_DAYS * DAILY_RATE)
double fine = elapsedDays - LOAN_DAYS * DAILY_RATE;   // 23.25, absurd

// RIGHT: the parentheses express the intent
double fine = (elapsedDays - LOAN_DAYS) * DAILY_RATE; // 3.0

The definitive advice: do not memorise the table. Use parentheses. Nobody has ever failed a code review for adding extra parentheses, and plenty of people have lost hours trusting precedence. Parentheses document your intent.

  1. Comparing strings: == versus equals

It is the classic Java mistake par excellence, and the worst part is that sometimes it works, which delays the diagnosis for weeks.

String a = "Effective Java";
String b = "Effective Java";
System.out.println(a == b);         // true  (!)

String c = new String("Effective Java");
System.out.println(a == c);         // false (!)
System.out.println(a.equals(c));    // true

Why it happens: the string pool

Remember from the previous lesson that a String variable does not contain the text, but a reference to an object on the heap. And == on references compares whether they point to the same object, not whether the content is equal.

The JVM maintains a string pool: a special area where it keeps literals. When it compiles String a = "Effective Java"; and then String b = "Effective Java";, it sees that the literal is identical and makes both variables point to the same object in the pool. That is why a == b gives true. It is a memory optimisation, not a promise of the language.

But new String("Effective Java") forces the creation of a new object, outside the pool. Now there are two objects with the same text at different addresses, and == gives false.

flowchart LR
    subgraph Stack
        A["a"]
        B["b"]
        C["c"]
    end
    subgraph Pool["String pool"]
        P["#p1<br/>'Effective Java'"]
    end
    subgraph Heap["Heap"]
        H["#h9<br/>'Effective Java'"]
    end
    A --> P
    B --> P
    C --> H

a == b compares #p1 == #p1 → true. a == c compares #p1 == #h9 → false. In both cases the text is the same.

The rule, without exceptions

To compare the content of two Strings, always use equals(). Never ==.

String enteredTitle = "effective java";
String catalogTitle = "Effective Java";

System.out.println(enteredTitle.equals(catalogTitle));             // false
System.out.println(enteredTitle.equalsIgnoreCase(catalogTitle));   // true

Why the danger is real: strings arriving from a Scanner, from a file or from the network are not in the pool. So your == comparison will work perfectly in tests with literals and fail with real data.

A common defensive technique is putting the literal on the left, because a literal is never null:

// If isbn were null, this would throw NullPointerException
if (isbn.equals("978-0000000001")) { ... }

// This is safe even if isbn is null
if ("978-0000000001".equals(isbn)) { ... }

Summary table:

Comparison With primitives With objects (String)
== Compares values. Correct Compares references. Almost always wrong
equals() Does not exist (primitives are not objects) Compares content. Correct

  1. The precision of double and why BigDecimal exists

One last warning, particularly relevant because BiblioTech handles money:

System.out.println(0.1 + 0.2);              // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);       // false
System.out.println(1.03 - 0.42);            // 0.6100000000000001

This is not a Java bug: it happens in Python, JavaScript, C and practically every language. The cause is that float and double follow the IEEE 754 standard, which represents numbers in binary. Just as in decimal you cannot write 1/3 exactly (0.3333…), in binary you cannot write 0.1 exactly. The stored value is an excellent approximation, but an approximation.

Practical consequences:

  • Never compare two doubles with ==. Compare whether their difference is smaller than a tolerance: Math.abs(a - b) < 0.0001.
  • Never use double for money in a real system. The errors accumulate over thousands of operations and end up throwing the accounts off.

Java's solution is the BigDecimal class, which represents decimals with exact precision and lets you control rounding:

import java.math.BigDecimal;

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));      // exactly 0.3

It has a cost: it is more verbose (you cannot use +, you have to call add()) and slower. It is studied in module 10; in this course we will keep using double for BiblioTech fines because the focus is on learning the language, but you have been warned: in a real billing system, BigDecimal.

Common Mistakes and Tips

  • 5 / 2 gives 2. Integer division. Apply a cast to one of the operands: (double) 5 / 2.
  • (double)(5 / 2) still gives 2.0. The cast arrives after the division. It must be applied to an operand, not to the result.
  • Comparing Strings with ==. It works with literals and fails with input data. Always use equals().
  • Comparing doubles with ==. Use a tolerance.
  • Forgetting the parentheses in (a - b) * c. Multiplication has higher priority than subtraction.
  • Confusing & with &&. On booleans, && avoids evaluating the second operand; & does not. On integers, & is a completely different bitwise operation.
  • Getting the order of guarded conditions wrong. x != null && x.length() > 0, never the other way round.
  • Writing acrobatic expressions with ++. If you have to stop and think, refactor it into two lines.
  • Tip: use parentheses whenever an expression has more than two operators. There is no performance penalty and the gain in clarity is enormous.
  • Tip: Integer.toBinaryString(n) is the best tool for understanding the bitwise operators. Try it in JShell.
  • Tip: the ternary is perfect for clamping values to zero (x > 0 ? x : 0), a pattern you will use a lot in BiblioTech so that the days late are never negative.

Exercises

Exercise 1: Predict the output

Without running the code, write the output of each line and justify why.

public class OperatorTest {
    public static void main(String[] args) {
        System.out.println(7 / 2);
        System.out.println(7 % 2);
        System.out.println(7.0 / 2);
        System.out.println((double)(7 / 2));

        int x = 5;
        System.out.println(x++);
        System.out.println(x);
        System.out.println(++x);

        System.out.println(2 + 3 * 4);
        System.out.println(10 - 4 - 3);

        String a = "Refactoring";
        String b = "Refactoring";
        String c = new String("Refactoring");
        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(a.equals(c));

        System.out.println(0.1 + 0.2 == 0.3);
        System.out.println(12 & 10);
        System.out.println(12 << 2);
    }
}

Exercise 2: The BiblioTech fine calculator

Write the class FineCalculator that, from the following constants and data, calculates and displays the settlement of a loan:

  • Constants: LOAN_DAYS = 15, DAILY_RATE = 0.25, MAX_FINE = 20.0, SEVERE_SURCHARGE = 1.5 (a multiplier applied if the delay exceeds 30 days).
  • Loan data: employee "Diego Alonso", book "Refactoring", elapsedDays = 52.

It must calculate and display:

  1. The days late, which can never be negative (use the ternary operator).
  2. The delay expressed in whole weeks and leftover days (use / and %).
  3. The base fine (daysLate * DAILY_RATE).
  4. The fine with the surcharge if the delay exceeds 30 days (use the ternary).
  5. The final fine, never exceeding MAX_FINE (use the ternary).
  6. A severity label: "ON TIME", "MINOR" (1-7 days) or "SEVERE" (more than 7), with nested ternaries.

All of it without using if.

Exercise 3: Permission flags

Using the bitwise operators, write the class BiblioTechPermissions that:

  1. Defines four permission constants with values 1, 2, 4 and 8: view, lend, reserve and administer.
  2. Composes the permissions of three Nexus Software employees: Marta Ruiz (view + lend), Diego Alonso (view + lend + reserve) and Nuria Vidal (all four).
  3. Shows for each one the integer value of their permissions, its binary representation and whether they can administer.
  4. Adds the reserve permission to Marta Ruiz using |= and shows the result.

Solutions

Solution 1

3
1
3.5
3.0
5
6
7
14
3
true
false
true
false
8
48

Justifications:

  • 7 / 2 → 3. Both operands are int: truncated integer division.
  • 7 % 2 → 1. The remainder of dividing 7 by 2.
  • 7.0 / 2 → 3.5. Since 7.0 is a double, the 2 is promoted and the division is a decimal one.
  • (double)(7 / 2) → 3.0. The parentheses force the integer division first (3) and the cast arrives afterwards.
  • x++ → 5, and immediately after x is 6. Post-increment returns the previous value.
  • x → 6. Confirms the earlier increment.
  • ++x → 7. Pre-increment increments first (7) and returns the new value.
  • 2 + 3 * 4 → 14. * has higher precedence than +: 3*4=12 is computed and then 2+12.
  • 10 - 4 - 3 → 3. Left associativity: (10-4)-3.
  • a == b → true. Both are identical literals, so the JVM shares them in the string pool and the two references point to the same object.
  • a == c → false. new String(...) creates a different object outside the pool; the references differ even though the text is the same.
  • a.equals(c) → true. equals compares the content, which is indeed identical.
  • 0.1 + 0.2 == 0.3 → false. The IEEE 754 representation of 0.1 and 0.2 is approximate; the sum gives 0.30000000000000004.
  • 12 & 10 → 8. In binary, 1100 & 1010 = 1000, which is 8.
  • 12 << 2 → 48. Shifting 2 bits to the left is equivalent to multiplying by 2² = 4.

Solution 2

public class FineCalculator {

    public static void main(String[] args) {

        // === BiblioTech business rules (Nexus Software) ===
        final int LOAN_DAYS = 15;              // loan days with no surcharge
        final double DAILY_RATE = 0.25;        // euros per day late
        final double MAX_FINE = 20.0;          // absolute cap per loan
        final double SEVERE_SURCHARGE = 1.5;   // multiplier if the delay passes 30 days
        final int SURCHARGE_THRESHOLD = 30;
        final int MINOR_THRESHOLD = 7;

        // === Loan data ===
        String employee = "Diego Alonso";
        String title = "Refactoring";
        String isbn = "978-0000000003";
        int elapsedDays = 52;

        // 1. Days late, clamped to zero with the ternary operator.
        //    If it were returned early, the subtraction would be negative and
        //    would produce a negative fine, which makes no sense.
        int daysLate = elapsedDays > LOAN_DAYS
                ? elapsedDays - LOAN_DAYS
                : 0;                                          // 52 - 15 = 37

        // 2. Breakdown into weeks and days: / gives the quotient, % the remainder.
        int weeks = daysLate / 7;                             // 5
        int leftoverDays = daysLate % 7;                      // 2

        // 3. Base fine. int * double -> Java promotes the int to a double.
        double baseFine = daysLate * DAILY_RATE;              // 37 * 0.25 = 9.25

        // 4. Surcharge for a severe delay, again with the ternary.
        double surchargedFine = daysLate > SURCHARGE_THRESHOLD
                ? baseFine * SEVERE_SURCHARGE
                : baseFine;                                   // 9.25 * 1.5 = 13.875

        // 5. Cap: the fine never exceeds MAX_FINE.
        double finalFine = surchargedFine > MAX_FINE
                ? MAX_FINE
                : surchargedFine;                             // 13.875 < 20 -> 13.875

        // 6. Severity label with nested ternaries.
        //    They are evaluated in a chain, top to bottom, like a ladder.
        String severity = daysLate == 0 ? "ON TIME"
                        : daysLate <= MINOR_THRESHOLD ? "MINOR"
                        : "SEVERE";

        // === Output ===
        System.out.println("=== BiblioTech - Loan settlement ===");
        System.out.println("Employee:        " + employee);
        System.out.println("Book:            " + title);
        System.out.println("ISBN:            " + isbn);
        System.out.println("Elapsed days: " + elapsedDays
                + " (standard loan: " + LOAN_DAYS + " days)");
        System.out.println();
        System.out.println("Days late:       " + daysLate
                + " (" + weeks + " weeks and " + leftoverDays + " days)");
        System.out.println("Severity:        " + severity);
        System.out.println("Base fine:       " + baseFine + " EUR");
        System.out.println("With surcharge:  " + surchargedFine + " EUR");
        System.out.println("FINAL FINE:      " + finalFine + " EUR");
    }
}

Output:

=== BiblioTech - Loan settlement ===
Employee:        Diego Alonso
Book:            Refactoring
ISBN:            978-0000000003
Elapsed days: 52 (standard loan: 15 days)

Days late:       37 (5 weeks and 2 days)
Severity:        SEVERE
Base fine:       9.25 EUR
With surcharge:  13.875 EUR
FINAL FINE:      13.875 EUR

Look at the 13.875: three decimals for an amount in euros. It is exactly the kind of result that in a real system would demand BigDecimal or, at the very least, formatting to two decimals. We will solve that last part in the next lesson with printf.

Solution 3

public class BiblioTechPermissions {

    public static void main(String[] args) {

        // 1. Each permission occupies ONE distinct bit: powers of 2.
        //    That way none interferes with the others when combined.
        final int VIEW        = 1;   // 0001
        final int LEND        = 2;   // 0010
        final int RESERVE     = 4;   // 0100
        final int ADMINISTER  = 8;   // 1000

        // 2. They are combined with OR: it turns on the bits of both operands.
        int martaPermissions = VIEW | LEND;                          // 0011 = 3
        int diegoPermissions = VIEW | LEND | RESERVE;                // 0111 = 7
        int nuriaPermissions = VIEW | LEND | RESERVE | ADMINISTER;   // 1111 = 15

        // 3. It is checked with AND: if the bit is on, the result is not zero.
        boolean martaAdmin = (martaPermissions & ADMINISTER) != 0;   // false
        boolean diegoAdmin = (diegoPermissions & ADMINISTER) != 0;   // false
        boolean nuriaAdmin = (nuriaPermissions & ADMINISTER) != 0;   // true

        System.out.println("=== BiblioTech - Catalog permissions ===");
        System.out.println("Marta Ruiz:   " + martaPermissions
                + " (" + Integer.toBinaryString(martaPermissions) + ")"
                + " admin=" + martaAdmin);
        System.out.println("Diego Alonso: " + diegoPermissions
                + " (" + Integer.toBinaryString(diegoPermissions) + ")"
                + " admin=" + diegoAdmin);
        System.out.println("Nuria Vidal:  " + nuriaPermissions
                + " (" + Integer.toBinaryString(nuriaPermissions) + ")"
                + " admin=" + nuriaAdmin);

        // 4. Adding a permission with |= : turns that bit on without touching the rest.
        martaPermissions |= RESERVE;                                 // 0011 | 0100 = 0111 = 7

        System.out.println();
        System.out.println("After granting RESERVE to Marta Ruiz:");
        System.out.println("Marta Ruiz:   " + martaPermissions
                + " (" + Integer.toBinaryString(martaPermissions) + ")"
                + " can reserve=" + ((martaPermissions & RESERVE) != 0));
    }
}

Output:

=== BiblioTech - Catalog permissions ===
Marta Ruiz:   3 (11) admin=false
Diego Alonso: 7 (111) admin=false
Nuria Vidal:  15 (1111) admin=true

After granting RESERVE to Marta Ruiz:
Marta Ruiz:   7 (111) can reserve=true

Two details worth attention:

  • The parentheses in (martaPermissions & ADMINISTER) != 0 are mandatory, not optional. & has lower precedence than !=, so without them Java would try to evaluate martaPermissions & (ADMINISTER != 0), which does not even compile because it mixes int and boolean.
  • Integer.toBinaryString does not show leading zeros: 3 appears as 11, not as 0011. That is normal; it represents the same value.

Conclusion

You now have a grip on Java's full operator catalogue: arithmetic with its integer-division trap, the modulo and its natural pairing with /, compound assignment with its hidden implicit cast, pre- and post-increment and when the difference matters, comparisons that produce booleans, the logical operators with the short-circuit evaluation that lets you guard dangerous expressions, the ternary for choosing values without if, bitwise operations applied to permission flags, precedence and the advice to use parentheses, the critical difference between == and equals explained by the string pool, and the precision limits of double that will one day lead you to BigDecimal. With all of that, BiblioTech can already calculate a complete fine: days late clamped to zero, a severity surcharge and a maximum cap.

In the next lesson, Console Input and Output, we will fix what has been left ugly in these examples: we will stop printing amounts like 13.875 and learn to format output with printf (%.2f, widths, alignment), and above all we will stop having the data hard-coded so we can ask the user for it with Scanner. It is the last ingredient before building the first complete version of BiblioTechApp.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved