Problem overview
A shop takes payments by card, by PayPal and in cash. Every payment has an amount in cents, and a total() that is the amount plus a processing fee. The total is worked out the same way for every payment. Only the fee depends on the kind.
That is what an abstract class is for. Payment holds the shared part (the amount and total()) and declares that every payment has a fee(), but it can't say what the fee is. Each subclass supplies its own:
| Class | Fee |
|---|---|
Card | amount * 2 / 100 (2%) |
PayPal | amount * 3 / 100 + 30 (3% plus 30 cents) |
Cash | 0 |
For Card(1999) the fee is 39, because the division drops the fraction (39.98 becomes 39), and the total is 2038. For PayPal(500) the fee is 45 and the total 545. For Cash(700) the total is 700. The three fees add up to 84, which is the second line the tests print for the first example.
Abstract classes in Java
A class marked abstract can't be created with new. It exists to be extended. It can have fields, constructors and ordinary methods like any class, and it can also declare abstract methods: a header ending in a semicolon, with no body, such as public abstract int fee();. Every concrete subclass must override each abstract method, or the compiler refuses it.
The constructor of an abstract class still matters. It runs when a subclass object is created, and the subclass starts it with super(amount), which is how the amount reaches the shared field.
The key line is in total(): it calls fee(), and Java picks the version that belongs to the object. For a Card that is the card's fee, even though total() is written only once in Payment.
You could ask why fee() isn't an ordinary method in Payment that returns 0. Then a subclass that forgot to write its own fee would compile and quietly charge nothing. With an abstract method, the compiler forces every kind of payment to decide its fee, and it makes a plain Payment impossible to create, which is right: there is no such thing as a payment of no particular kind.
Step by step
- Declare the shared field in
Payment:public int amount;. Card, PayPal and Cash all inherit it. - Store it in the constructor with
this.amount = amount;. - Declare
fee()as abstract:public abstract int fee();, with a semicolon instead of a body. - Write
total()asamount + fee(). - Pass the amount up in each subclass constructor with
super(amount);. - Override
fee()in each subclass:amount * 2 / 100for a card,amount * 3 / 100 + 30for PayPal and0for cash.
The solution in Java
abstract class Payment {
public int amount;
public Payment(int amount) {
this.amount = amount;
}
public abstract int fee();
public int total() {
return amount + fee();
}
}
class Card extends Payment {
public Card(int amount) {
super(amount);
}
@Override
public int fee() {
return amount * 2 / 100;
}
}
class PayPal extends Payment {
public PayPal(int amount) {
super(amount);
}
@Override
public int fee() {
return amount * 3 / 100 + 30;
}
}
class Cash extends Payment {
public Cash(int amount) {
super(amount);
}
@Override
public int fee() {
return 0;
}
}
Integer division and the order of operations
All the values are int, so / drops the fraction. Multiply first and divide last: amount * 2 / 100 is evaluated from left to right, so it is (amount * 2) / 100. For an amount of 150 that is 3. Dividing first, as in amount / 100 * 2, gives 1 * 2 = 2, because the division loses the 50 too early.
For PayPal the 30 is added after the division. With an amount of 33, 33 * 3 / 100 is 0, so the fee is exactly 30. With an amount of 0 it is still 30.
Why the amounts stay safe
The amount is at most 1,000,000 cents, so amount * 3 is at most 3,000,000, far below the largest int (2,147,483,647). A Card(1000000) totals 1020000 and a PayPal(1000000) totals 1030030.
Using the payments through the abstract type
Because every payment is a Payment, one array can hold all the kinds, and the code that uses them never names Card, PayPal or Cash:
Payment[] payments = { new Card(1999), new PayPal(500), new Cash(700) };
int fees = 0;
for (Payment payment : payments) {
System.out.println(payment.total());
fees += payment.fee();
}
System.out.println("fees: " + fees);
2038
545
700
fees: 84
A fourth kind of payment, say a bank transfer with a flat fee, only needs a new subclass with its own fee(). total() and this loop stay as they are.
Testing
| Payments | Totals | Sum of fees |
|---|---|---|
Card(1999), PayPal(500), Cash(700) | 2038 545 700 | 84 |
Card(49), PayPal(33), Card(50) | 49 63 51 | 31 |
Card(0), PayPal(0), Cash(0) | 0 30 0 | 30 |
In the second row, 49 cents on a card is 0.98 cents of fee, which rounds down to 0, and 50 cents is exactly 1.
Common mistakes
- Trying to create a plain
Payment.new Payment(100)is a compile error:Payment is abstract; cannot be instantiated. - Leaving
fee()out of a subclass. A class such asCheck extends Paymentthat doesn't override it fails to compile withCheck is not abstract and does not override abstract method fee() in Payment. Cash has no fee, but it still has to writefee()and return 0. - Forgetting
super(amount).Paymenthas only a constructor that takes an amount, so every subclass constructor must call it. Without the call, the subclass doesn't compile. - Dividing before multiplying.
amount / 100 * 2gives 2 for an amount of 150, while the correct fee is 3. - Overriding
total(). The lesson keepstotal()inPaymentonly, so that the total is computed in one place. - Forgetting the 30 cents at amount 0. PayPal's fee is not a percentage alone; the 30 is always added.
Complexity
Each fee() and each total() is a few arithmetic operations, so it takes O(1) time and no extra space.
