When you finished the previous lesson your objects were boxes of data: Book holds a title and an ISBN, but it does not know how to do absolutely anything with them. All of BiblioTech's intelligence is still locked inside the two hundred lines of main, which calculates days late, applies the fine cap and classifies the severity as if the objects did not exist. This lesson supplies the missing half: behaviour. A method is a named operation that groups a set of instructions together, receives data, does a job and normally returns a result. It is the unit of code decomposition —what stops main from growing endlessly— and, in OOP, it is the way an object exposes what it knows how to do. By the end, calculateFine() will not be a loose block in the middle of a switch: it will be something a Loan knows how to do to itself.

Contents

  1. Anatomy of a method
  2. The return value and return
  3. void methods
  4. Parameter passing: Java always passes by value
  5. What that means with primitives
  6. What that means with references (and why it confuses everyone)
  7. Method overloading
  8. static methods versus instance methods
  9. The this reference
  10. Varargs: a variable number of arguments
  11. Variable scope and shadowing
  12. Good practice in method design
  13. Recursion: a method that calls itself
  14. Guided refactoring of BiblioTech
  15. Common Mistakes and Tips
  16. Exercises

  1. Anatomy of a method

This is the complete structure of a Java method:

public double calculateFine(int daysLate) {
    return daysLate * DAILY_RATE;
}
Part In the example What it means
Access modifier public Who can call it (03-07)
Other modifiers (none) static, final, abstract...
Return type double What type of value it returns; void if it returns nothing
Name calculateFine A verb in lowerCamelCase
Parameter list (int daysLate) Input data, with type and name
Body { return ...; } The instructions that are executed

The combination name + parameter types in order is called the method's signature. This one's signature is calculateFine(int). The return type is not part of the signature, and that will have consequences in section 7.

Vocabulary worth nailing down so you never confuse it again:

  • Parameter: the variable declared in the method definition (int daysLate).
  • Argument: the specific value passed when calling it (calculateFine(5) passes the argument 5).

Calling an instance method is always done through an object:

double fine = loan.calculateFine(5);

  1. The return value and return

return does two things at once: it hands a value back to the caller and immediately ends the method's execution. You saw the second part in lesson 02-04, applied to flow control.

public String classifySeverity(int daysLate) {
    if (daysLate == 0) {
        return "ON TIME";          // exits right here
    }
    if (daysLate <= MINOR_THRESHOLD) {
        return "MINOR";
    }
    return "SEVERE";
}

Compiler rules you must know:

  • The returned value must be compatible with the declared type. A double method can return an int (automatic promotion), but an int method cannot return a double without an explicit cast.
  • Every path must return a value. If the compiler finds a route without a return, it fails with "missing return statement".
  • Code after a reachable return is always unreachable code and does not compile.

This method, for instance, does not compile:

public String classify(int days) {
    if (days > 0) {
        return "LATE";
    }
    // ERROR: if days <= 0 there is no return
}

Adding an else or a final return is enough to fix it. The second form —using guard clauses and leaving the general case at the end, without else— is the one you used in 02-01 and the one preferred in professional code.

  1. void methods

When a method does something instead of calculating something, its return type is void:

public void lend() {
    available = false;
}

In a void method you can use a bare return; to exit early, but you cannot return a value:

public void returnItem() {
    if (available) {
        System.out.println("The book was already available.");
        return;                    // early exit, with no value
    }
    available = true;
}

A useful design criterion, known as command-query separation: a method either changes the state (and returns void) or answers a question (and returns a value), but preferably not both. A method called getFine() that also marked the book as returned would surprise anyone reading it.

  1. Parameter passing: Java always passes by value

Here is one of the points where most people go wrong, even with years of experience. The rule is this, and it has no exceptions:

In Java, arguments are always passed by value. That is: when calling a method, the value of the argument is copied into the parameter.

What throws people off is what exactly is being copied in each case:

Argument type What is copied Consequence
Primitive (int, double, boolean...) The value itself The method works with an independent copy
Reference (String, Book, Loan...) The address, not the object The method reaches the same object, but with its own arrow

Hence the two consequences you have to memorise:

  1. Reassigning the parameter never affects the caller (neither with primitives nor with objects).
  2. Mutating the object the parameter points to does affect the caller (because it is the same object).

The next two sections demonstrate it with code.

  1. What that means with primitives

public static void tryToModify(int days) {
    days = 999;                       // only changes the local copy
    System.out.println("  Inside the method: " + days);
}

public static void main(String[] args) {
    int elapsedDays = 20;
    System.out.println("Before:  " + elapsedDays);
    tryToModify(elapsedDays);
    System.out.println("After:   " + elapsedDays);
}

Output:

Before:  20
  Inside the method: 999
After:   20

The parameter days is a new variable on the method's stack, initialised with a copy of 20. Changing it does not touch the original variable. If a method must "modify" a primitive, the only way is to return the new value:

elapsedDays = increment(elapsedDays);

  1. What that means with references (and why it confuses everyone)

Now the interesting case. Two apparently similar methods, with opposite results:

public static void mutateBook(Book book) {
    book.title = "TITLE CHANGED";       // mutates the shared object
}

public static void reassignBook(Book book) {
    book = new Book();                  // repoints ONLY the local arrow
    book.title = "ANOTHER BOOK";
}

public static void main(String[] args) {
    Book effectiveJava = new Book();
    effectiveJava.title = "Effective Java";

    mutateBook(effectiveJava);
    System.out.println("After mutating:    " + effectiveJava.title);

    reassignBook(effectiveJava);
    System.out.println("After reassigning: " + effectiveJava.title);
}

Output:

After mutating:    TITLE CHANGED
After reassigning: TITLE CHANGED

The second call did nothing visible. Let us see why, in memory:

flowchart TB
    subgraph P1["Stack of main"]
        M["effectiveJava = 0x1A"]
    end
    subgraph P2["Stack of reassignBook"]
        L1["book = 0x1A (on entry)"]
        L2["book = 0x9F (after new)"]
    end
    subgraph H["Heap"]
        O1["0x1A : Book
        title = TITLE CHANGED"]
        O2["0x9F : Book
        title = ANOTHER BOOK"]
    end
    M --> O1
    L1 -.-> O1
    L2 --> O2

On entering reassignBook, the parameter book receives a copy of the address 0x1A: there are two arrows pointing at the same object. When the method does book = new Book(), it moves its own arrow to the new object 0x9F. main's arrow has not been touched. On leaving the method, the local stack disappears and the object 0x9F becomes unreachable.

In mutateBook, by contrast, no arrow is touched: the dot is used to modify the object both of them point to, and that is why the change is visible from main.

The sentence that sums it all up: you can change the contents of the object you are given, but you cannot change which object the caller's variable points to.

A practical corollary: String is immutable (module 1), so no method can alter the string you pass it. This never works:

public static void spoil(String text) {
    text = text.toUpperCase();   // creates another string; the caller never finds out
}

  1. Method overloading

To overload is to declare several methods with the same name and a different parameter list in the same class. Java picks which one to call based on the arguments.

public class Loan {

    /** Fine for this loan, using its own elapsed days. */
    public double calculateFine() {
        return calculateFine(elapsedDays);
    }

    /** Fine corresponding to a specific number of elapsed days. */
    public double calculateFine(int elapsedDays) {
        int late = Math.max(0, elapsedDays - LOAN_DAYS);
        return Math.min(late * DAILY_RATE, MAX_FINE);
    }

    /** Fine with a different rate (for example, in a special campaign). */
    public double calculateFine(int elapsedDays, double rate) {
        int late = Math.max(0, elapsedDays - LOAN_DAYS);
        return Math.min(late * rate, MAX_FINE);
    }
}

What makes an overload valid:

Change Valid overload?
Different number of parameters Yes
Different parameter types Yes
Different order of different types Yes ((int, String) versus (String, int))
Only the parameter names differ No: compile error
Only the return type differs No: the return type is not part of the signature

Resolution rules, which Java applies in this order until it finds a match:

  1. Exact match of types.
  2. Primitive type promotion (int → long → float → double).
  3. Autoboxing/unboxing (int ↔ Integer, module 1).
  4. Varargs (section 10), which is always the last option considered.
public static void register(int days)    { System.out.println("int version"); }
public static void register(double days) { System.out.println("double version"); }
public static void register(Object days) { System.out.println("Object version"); }

register(5);      // "int version"     -> exact match
register(5.0);    // "double version"  -> exact match
register(5L);     // "double version"  -> long promotes to double before Object

Tip: overloading is useful when the variants do conceptually the same thing with different data. If two methods with the same name do different things, give them different names.

  1. static methods versus instance methods

The difference is the same one you saw with fields in 03-02:

Aspect Instance method static method
Belongs to A specific object The class
How it is called loan.calculateFine() Loan.isMinorDelay(5)
Can use instance fields Yes No (there is no object)
Can use this Yes No
Can use static fields Yes Yes
When to use it The operation depends on the object's state The operation depends only on its parameters

Real-world examples you already know: Math.max(a, b) and Integer.parseInt(text) are static because they need no state; scanner.nextLine() is an instance method because it depends on the state of that Scanner. And main is static because the JVM must be able to call it before any object exists.

Applied to BiblioTech:

public class Loan {

    /** Instance: uses THIS loan's data. */
    public int calculateDaysLate() {
        return Math.max(0, elapsedDays - LOAN_DAYS);
    }

    /** Static: a pure function of its parameters, useful without having a loan. */
    public static boolean isMinorDelay(int daysLate) {
        return daysLate > 0 && daysLate <= MINOR_THRESHOLD;
    }
}

The classic beginner's mistake, and probably the compiler message you will see most often:

public class Loan {
    public int elapsedDays;

    public static int wrong() {
        return elapsedDays;   // ERROR
    }
}
error: non-static variable elapsedDays cannot be referenced from a static context

The translation is literal: a static method is not associated with any object, so the question "whose elapsed days?" has no answer.

  1. The this reference

Inside an instance method, this is a reference to the object the method was called on. It has three uses:

a) Disambiguating when a parameter has the same name as a field (the most frequent use, and the one you will see constantly in constructors):

public void setElapsedDays(int elapsedDays) {
    this.elapsedDays = elapsedDays;   // field = parameter
}

Without this, elapsedDays = elapsedDays; assigns the parameter to itself and the field does not change. The compiler does not complain; the error is silent.

b) Calling another method on the same object (optional, but sometimes more readable):

public String buildSummary() {
    return this.classifySeverity() + " - " + this.calculateFine() + " EUR";
}

c) Passing the current object to another method:

public void announce(AuditLog log) {
    log.record(this);   // "here I am"
}

Outside those cases, writing this. in front of everything is noise. In Java the dominant style is to omit it except when necessary. In 03-04 a fourth form will appear, this(...), for chaining constructors.

  1. Varargs: a variable number of arguments

Varargs let you declare a parameter that accepts zero, one or many arguments of the same type. They are written with three dots:

public static void logEvents(String... events) {
    System.out.println("Events received: " + events.length);
    for (String event : events) {
        System.out.println("  - " + event);
    }
}

Valid calls:

logEvents();                                  // 0 events
logEvents("Loan registered");                 // 1
logEvents("Return", "Fine applied");          // 2

Inside the method, events is an array (String[]), with its length and its for-each loop. Arrays are studied in depth in lesson 05-01; here it is enough to know they are traversed like this.

Two mandatory rules:

  • There can be only one varargs parameter per method.
  • It must be the last in the list: register(String employee, String... events) is valid; the other way round does not compile.

You have already used varargs without knowing it: System.out.printf(String format, Object... args) is exactly this, and that is why it accepts any number of values after the format string.

  1. Variable scope and shadowing

A variable's scope is the region of code where it is visible. Summary:

Where it is declared Where it is visible from How long it lives
Instance field Every method in the class As long as the object lives
static field Every method, including static ones The whole program
Parameter Only inside its method The method's execution
Local variable From its declaration to the } of its block The block's execution

Shadowing happens when a local variable or a parameter has the same name as a field: inside that scope, the name refers to the local variable, and the field is left "in the shadow".

public class Loan {
    public int elapsedDays = 20;   // field

    public void demonstrateShadowing(int elapsedDays) {   // shadowing parameter
        System.out.println(elapsedDays);        // the PARAMETER
        System.out.println(this.elapsedDays);   // the FIELD
    }
}
new Loan().demonstrateShadowing(5);
// Output:
// 5
// 20

Shadowing is not an error —in fact it is standard practice in constructors and setters, where the parameter is deliberately given the same name as the field—, but it demands that you remember this. when assigning.

  1. Good practice in method design

These criteria separate a professional method from an improvised one:

  • One method, one purpose. If describing what it does requires the word "and", it is probably two methods.
  • Short. There is no magic number, but if a method does not fit on the screen, it is hard to reason about. The two hundred lines of main are the perfect counterexample.
  • Verb-based, precise names. calculateFine, registerReturn, isAvailable. Avoid process, manage, handle or data.
  • Boolean naming convention: isX, hasX, canX. if (book.isAvailable()) reads like a sentence.
  • Few parameters. With more than three or four, consider grouping related data into an object. It is, once again, the problem of loose variables.
  • Avoid boolean parameters. generateReport(true) says nothing when you read it. What is true?
// Bad: what does that false mean?
printReceipt(loan, false);

// Better: two methods with explicit names
printFullReceipt(loan);
printSummaryReceipt(loan);
  • Do not mix abstraction levels. A method that calculates business rules should not also format with printf. It is the central topic of lesson 03-08.
  • Document with Javadoc whatever is not obvious: what it returns, what it expects and what happens at the edge cases.

  1. Recursion: a method that calls itself

A method can call itself. That is called recursion, and every correct recursion has two parts: a base case that stops it and a recursive case that moves towards the base case.

/** Adds 1 + 2 + ... + n recursively. */
public static int sumUpTo(int n) {
    if (n <= 0) {          // base case: stops the recursion
        return 0;
    }
    return n + sumUpTo(n - 1);      // recursive case
}

Trace of sumUpTo(4):

sumUpTo(4) = 4 + sumUpTo(3)
           = 4 + (3 + sumUpTo(2))
           = 4 + (3 + (2 + sumUpTo(1)))
           = 4 + (3 + (2 + (1 + sumUpTo(0))))
           = 4 + 3 + 2 + 1 + 0 = 10

Each pending call occupies a frame on the stack. If the base case is missing or never reached, the stack runs out and you get a StackOverflowError, which you will recognise because the trace repeats the same line hundreds of times.

Day to day, recursion shines with structures that nest by nature —trees, directories, expressions— and for everything else a loop is usually preferable, since it uses less memory. You will see it applied in lesson 05-09 (binary search) and in module 7 (walking directories).

  1. Guided refactoring of BiblioTech

The time has come to apply all this to the project. We will do it in two steps, because that is how it is done in real life: first extract, then move.

Step 1: extract the calculations from main into static methods

This is the code that lives today inside main's switch:

// --- BEFORE: inside main, in the middle of case "1" ---
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate < 0) {
    daysLate = 0;
}

double fine = daysLate * DAILY_RATE;
if (fine >= MAX_FINE) {
    fine = MAX_FINE;
}

String severity;
if (daysLate == 0) {
    severity = "ON TIME";
} else if (daysLate <= MINOR_THRESHOLD) {
    severity = "MINOR";
} else {
    severity = "SEVERE";
}

First improvement: pull it out into named methods.

// --- AFTER (step 1): static methods in BiblioTechApp ---

/**
 * Calculates the days late, clamped to zero.
 * @param elapsedDays days since the loan was made
 * @return 0 if it is still within term; the excess days otherwise
 */
private static int calculateDaysLate(int elapsedDays) {
    return Math.max(0, elapsedDays - LOAN_DAYS);
}

/**
 * Calculates the fine for a delay, applying the legal cap.
 * @param daysLate days late, never negative
 * @return amount in euros, never greater than MAX_FINE
 */
private static double calculateFine(int daysLate) {
    return Math.min(daysLate * DAILY_RATE, MAX_FINE);
}

/**
 * Classifies the severity of a delay according to Nexus Software's policy.
 * @param daysLate days late, never negative
 * @return "ON TIME", "MINOR" or "SEVERE"
 */
private static String classifySeverity(int daysLate) {
    if (daysLate == 0)                return "ON TIME";
    if (daysLate <= MINOR_THRESHOLD)  return "MINOR";
    return "SEVERE";
}

And case "1" ends up like this:

int    daysLate = calculateDaysLate(elapsedDays);
double fine     = calculateFine(daysLate);
String severity = classifySeverity(daysLate);

Three readable lines instead of twenty. Notice the use of Math.max and Math.min, which replace the clamping ifs: Math.max(0, x) is "never below zero" and Math.min(x, cap) is "never above the cap". Less code and fewer places to get it wrong.

Step 2: move the methods to Loan

Step 1 improves readability, but leaves the business rules in the startup class. And those rules do not belong to the application: they belong to the loan. This is the genuinely object-oriented step.

package com.nexussoftware.bibliotech.domain;

/**
 * Record of a loan of a book to a Nexus Software employee.
 * It knows its own data and knows how to apply the business rules to it.
 */
public class Loan {

    public static final int    LOAN_DAYS       = 15;
    public static final double DAILY_RATE      = 0.25;
    public static final double MAX_FINE        = 20.0;
    public static final int    MINOR_THRESHOLD = 7;

    public static int loansCreated = 0;

    public Book   book;
    public String employee;
    public int    elapsedDays;

    // ---------- Instance behaviour ----------

    /** @return days late for THIS loan, clamped to zero. */
    public int calculateDaysLate() {
        return Math.max(0, elapsedDays - LOAN_DAYS);
    }

    /** @return fine for THIS loan in euros, with the cap applied. */
    public double calculateFine() {
        return Math.min(calculateDaysLate() * DAILY_RATE, MAX_FINE);
    }

    /** @return "ON TIME", "MINOR" or "SEVERE" according to THIS loan's delay. */
    public String classifySeverity() {
        int late = calculateDaysLate();
        if (late == 0)                return "ON TIME";
        if (late <= MINOR_THRESHOLD)  return "MINOR";
        return "SEVERE";
    }

    /** @return true if the 15-day term has already been exceeded. */
    public boolean isOverdue() {
        return calculateDaysLate() > 0;
    }

    /** @return true if the fine has reached the MAX_FINE cap. */
    public boolean hasCappedFine() {
        return calculateFine() >= MAX_FINE;
    }

    /** @return days left before it falls due; 0 if it is already due. */
    public int daysRemaining() {
        return Math.max(0, LOAN_DAYS - elapsedDays);
    }

    // ---------- Static utility ----------

    /**
     * Says whether a given delay is considered minor, without needing to have
     * a loan built. Useful for tables and simulations.
     */
    public static boolean isMinorDelay(int daysLate) {
        return daysLate > 0 && daysLate <= MINOR_THRESHOLD;
    }
}

And now, the before and after of the calculation in main:

Before (module 2) After (this module)
20 lines inside case "1" loan.calculateFine()
The rules are repeated in the fine scale They exist only once, in Loan
Changing the rate means reviewing all of main You change one constant
The data travels loose through parameters It travels inside the object
Only the "current" loan can be calculated Any loan can be calculated

Use from BiblioTechApp:

Loan loan = new Loan();
loan.book        = refactoring;
loan.employee    = "Marta Ruiz";
loan.elapsedDays = 20;
Loan.loansCreated++;

System.out.printf("Book:      %s%n",       loan.book.title);
System.out.printf("Employee:  %s%n",       loan.employee);
System.out.printf("Days late: %d days%n",  loan.calculateDaysLate());
System.out.printf("Fine:      %.2f EUR%n", loan.calculateFine());
System.out.printf("Severity:  %s%n",       loan.classifySeverity());
System.out.printf("Overdue:   %b%n",       loan.isOverdue());

Output:

Book:      Refactoring
Employee:  Marta Ruiz
Days late: 5 days
Fine:      1.25 EUR
Severity:  MINOR
Overdue:   true

This is the moment BiblioTech becomes genuinely object-oriented: main no longer calculates anything, it just asks. And notice that calculateFine() internally calls calculateDaysLate(): a method can lean on another one of the same object, avoiding duplication of the clamping logic.

Let us also add behaviour to Book, which until now was pure data:

public class Book {

    public static int booksCreated = 0;

    public String  title;
    public String  author;
    public String  isbn;
    public int     publicationYear;
    public boolean available;

    /** @return true if the copy can be lent right now. */
    public boolean isAvailable() {
        return available;
    }

    /** Marks the copy as on loan. Warns if it already was. */
    public void lend() {
        if (!available) {
            System.out.println("WARNING: '" + title + "' was already on loan.");
            return;
        }
        available = false;
    }

    /** Marks the copy as returned. Warns if it was already on the shelf. */
    public void returnItem() {
        if (available) {
            System.out.println("WARNING: '" + title + "' was already available.");
            return;
        }
        available = true;
    }

    /** @return a readable description of the book for listings. */
    public String describe() {
        return title + " - " + author + " (" + publicationYear + "), ISBN " + isbn;
    }
}

Look at lend() and returnItem(): they are the whole reason encapsulation exists. With a public field, anyone can write book.available = true; skipping the warning; with these methods, the operation goes through a single place that can validate. Lesson 03-07 will close that door for good.

Common Mistakes and Tips

  • Forgetting this in a setter. elapsedDays = elapsedDays; compiles, does nothing and gives no warning. It is one of the most frustrating errors: the IDE flags it as "assignment to itself", listen to it.
  • Believing that Java passes objects by reference. It does not: it passes the reference by value. The difference shows up exactly when you reassign the parameter, as in section 6.
  • Trying to use instance fields from a static method. The compiler will say "non-static variable ... cannot be referenced from a static context". Ask yourself whether the method should be an instance method or whether it is missing the object as a parameter.
  • Overloading by changing only the return type. It does not compile: the signature does not include the return.
  • Methods that do two things. calculateAndSaveFine() is a warning sign in the name itself.
  • Returning magic numeric codes. Returning -1 to mean "could not be calculated" forces every caller to know that convention. Until you have exceptions (module 6) or Optional (10-04), document the convention explicitly with Javadoc.
  • Tip: use your IDE's "extract method". IntelliJ (Ctrl+Alt+M) and Eclipse (Alt+Shift+M) do step 1's refactoring automatically and without errors: you select the block, give it a name, and the IDE works out the parameters and the return.
  • Tip: write the signature and the Javadoc first, the body afterwards. It forces you to decide what goes in and what comes out before getting lost in the implementation.

Exercises

Exercise 1: behaviour in Employee

Extend the Employee class from the previous lesson with these methods:

  • registerLoan(): increments totalLoans by one. If the employee is already at the MAX_CONCURRENT_LOANS limit, it does not increment and warns on the console.
  • registerReturn(): decrements totalLoans, never below zero.
  • canBorrow(): returns true if the maximum has not been reached yet.
  • getInitials(): returns the employee's initials ("Marta Ruiz" → "M.R.").
  • An overloaded version registerLoan(int amount) that registers several at once while respecting the limit.

Test them with Marta Ruiz until she reaches the limit.

Exercise 2: demonstrate pass-by-value

Write a class PassByValueDemo with four static methods that receive, respectively: an int, a String, a Book that is mutated and a Book that is reassigned. Each one must print the value on entry and on exit. From main, call all four and print the state before and after each call. Then answer in writing: why does the String case behave like the int one and not like the mutated Book?

Exercise 3: fine scale simulator with varargs

Write in Loan a static method simulateScale(int... elapsedDays) that receives any number of elapsed-day values and prints a table with: elapsed days, days late, uncapped fine, applied fine and severity. It must mark with <-- the first row where the fine reaches the cap. Reuse the existing methods and do not duplicate any business rule. Test it with simulateScale(5, 15, 20, 30, 60, 95, 100).

Solutions

Solution 1

package com.nexussoftware.bibliotech.domain;

public class Employee {

    public static final int MAX_CONCURRENT_LOANS = 3;
    public static int employeesRegistered = 0;

    public String name;
    public String identifier;
    public int    totalLoans;

    /** @return true if another book can still be taken out. */
    public boolean canBorrow() {
        return totalLoans < MAX_CONCURRENT_LOANS;
    }

    /** Registers a loan if the limit allows it. */
    public void registerLoan() {
        if (!canBorrow()) {
            System.out.printf("WARNING: %s already has %d loans (maximum %d).%n",
                              name, totalLoans, MAX_CONCURRENT_LOANS);
            return;                                  // early exit
        }
        totalLoans++;
    }

    /**
     * Registers several loans at once. Overloaded version: the same
     * conceptual operation with different input.
     */
    public void registerLoan(int amount) {
        for (int i = 0; i < amount; i++) {
            registerLoan();            // reuses the logic and the limit
        }
    }

    /** Registers a return, never dropping below zero. */
    public void registerReturn() {
        totalLoans = Math.max(0, totalLoans - 1);
    }

    /** @return the initials of the full name, for example "M.R." */
    public String getInitials() {
        String[] parts = name.trim().split(" ");
        StringBuilder sb = new StringBuilder();       // pattern from lesson 02-02
        for (String part : parts) {
            if (!part.isEmpty()) {
                sb.append(part.charAt(0)).append('.');
            }
        }
        return sb.toString().toUpperCase();
    }
}

Test:

Employee marta = new Employee();
marta.name       = "Marta Ruiz";
marta.identifier = "EMP-001";

System.out.println(marta.getInitials());     // M.R.
marta.registerLoan(2);                       // 2 loans
System.out.println(marta.totalLoans);        // 2
System.out.println(marta.canBorrow());       // true
marta.registerLoan();                        // 3
marta.registerLoan();                        // WARNING: limit reached
System.out.println(marta.totalLoans);        // 3
marta.registerReturn();
System.out.println(marta.totalLoans);        // 2

Output:

M.R.
2
true
WARNING: Marta Ruiz already has 3 loans (maximum 3).
3
2

Keys: registerLoan(int) does not duplicate the limit check, it delegates to the no-parameter version; and Math.max(0, ...) in the return avoids a negative counter without needing an if.

Solution 2

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domain.Book;

public class PassByValueDemo {

    static void withPrimitive(int days) {
        System.out.println("   in:  " + days);
        days = 999;
        System.out.println("   out: " + days);
    }

    static void withString(String title) {
        System.out.println("   in:  " + title);
        title = title.toUpperCase();     // creates ANOTHER string; only the local copy changes
        System.out.println("   out: " + title);
    }

    static void mutatingObject(Book book) {
        System.out.println("   in:  " + book.title);
        book.title = "MUTATED";          // touches the shared object
        System.out.println("   out: " + book.title);
    }

    static void reassigningObject(Book book) {
        System.out.println("   in:  " + book.title);
        book = new Book();               // moves ONLY the local arrow
        book.title = "NEW OBJECT";
        System.out.println("   out: " + book.title);
    }

    public static void main(String[] args) {

        int days = 20;
        System.out.println("1) Primitive. Before: " + days);
        withPrimitive(days);
        System.out.println("   After: " + days);            // 20

        String title = "Effective Java";
        System.out.println("2) String. Before: " + title);
        withString(title);
        System.out.println("   After: " + title);           // Effective Java

        Book book = new Book();
        book.title = "Design Patterns";
        System.out.println("3) Object mutated. Before: " + book.title);
        mutatingObject(book);
        System.out.println("   After: " + book.title);      // MUTATED

        Book other = new Book();
        other.title = "Refactoring";
        System.out.println("4) Object reassigned. Before: " + other.title);
        reassigningObject(other);
        System.out.println("   After: " + other.title);     // Refactoring
    }
}

Output:

1) Primitive. Before: 20
   in:  20
   out: 999
   After: 20
2) String. Before: Effective Java
   in:  Effective Java
   out: EFFECTIVE JAVA
   After: Effective Java
3) Object mutated. Before: Design Patterns
   in:  Design Patterns
   out: MUTATED
   After: MUTATED
4) Object reassigned. Before: Refactoring
   in:  Refactoring
   out: NEW OBJECT
   After: Refactoring

Answer to the question. The String behaves like the int because it is immutable: toUpperCase() does not modify the original string, it creates a new one and returns it. Assigning it to the parameter only moves the local arrow, exactly as in case 4. It is not that Strings are passed differently —they are passed by value of the reference, like any object—, it is that there is no way to mutate them, so case 3 is impossible with String. That is one of the great advantages of immutability, and the reason why in 03-07 you will see that Book is a good candidate to become immutable too.

Solution 3

/**
 * Prints the fine scale for a series of elapsed-day values.
 * @param elapsedDays zero or more values to simulate
 */
public static void simulateScale(int... elapsedDays) {

    System.out.println("  BIBLIOTECH FINE SCALE");
    System.out.println("  ==================================================");
    System.out.printf("  %8s %9s %11s %11s  %-11s%n",
                      "ELAPSED", "LATE", "UNCAPPED", "APPLIED", "SEVERITY");

    boolean capAlreadyMarked = false;       // flag pattern (lesson 02-02)

    for (int days : elapsedDays) {

        // The SAME domain logic is reused: zero duplication of rules.
        int    late     = Math.max(0, days - LOAN_DAYS);
        double uncapped = late * DAILY_RATE;
        double applied  = Math.min(uncapped, MAX_FINE);

        String severity;
        if (late == 0)                  severity = "ON TIME";
        else if (isMinorDelay(late))    severity = "MINOR";
        else                            severity = "SEVERE";

        String mark = "";
        if (!capAlreadyMarked && applied >= MAX_FINE) {
            mark = "  <--";
            capAlreadyMarked = true;        // once raised, it never drops again
        }

        System.out.printf("  %8d %9d %11.2f %11.2f  %-11s%s%n",
                          days, late, uncapped, applied, severity, mark);
    }
}

Call and output:

Loan.simulateScale(5, 15, 20, 30, 60, 95, 100);
  BIBLIOTECH FINE SCALE
  ==================================================
   ELAPSED      LATE    UNCAPPED     APPLIED  SEVERITY
         5         0        0.00        0.00  ON TIME
        15         0        0.00        0.00  ON TIME
        20         5        1.25        1.25  MINOR
        30        15        3.75        3.75  SEVERE
        60        45       11.25       11.25  SEVERE
        95        80       20.00       20.00  SEVERE       <--
       100        85       21.25       20.00  SEVERE

Three details deserve attention. First, the method is static because it simulates hypothetical situations: there is no real loan behind it, just numbers. Second, it reuses isMinorDelay(int), also static, without duplicating the threshold. And third, simulateScale() with no arguments is a perfectly valid call: it prints just the header, because varargs accepts zero elements.

One detail worth discussing: this method mixes calculation and presentation, exactly what section 12 advises against. It is acceptable in a diagnostic utility like this one, but remember it: in lesson 03-08 you will see why in business code that mixture is expensive.

Conclusion

Your objects now know how to do things. You master the complete anatomy of a method —modifiers, return type, signature, parameters, body— and the difference between parameter and argument; you know when a method returns a value and when it is void, and why it had better not do both. You have understood the rule that generates the most confusion in Java: everything is passed by value, including objects, whose reference is copied; hence mutating the received object affects the caller and reassigning the parameter does not. You know how to overload methods and you know the rules by which Java picks the right version, you tell static methods from instance methods and you understand why a static one cannot touch the state of an object it does not have. You handle this, varargs, variable scope and shadowing, and you have a catalogue of good practices for writing methods others can read. You have seen recursion with its base case and its risk of StackOverflowError.

And, above all, you have carried out BiblioTech's first big refactoring: the delay, fine and severity calculations have left main, become named methods and then moved to the Loan class, their rightful owner. main no longer calculates: it asks.

One loose end remains, an obvious one. Your objects are still born empty: new Loan() produces a loan with no book, no employee and zero days, and it has to be filled in field by field trusting that you forget none of them. In the next lesson, Constructors, you will close that door: you will learn to demand the essential data at the moment of creation, to overload and chain constructors, to control the order of initialisation and to validate the data so that no BiblioTech object can be born in an invalid state.

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