Skip to lab content

WEEK 5 · OBJECT ORIENTED PROGRAMMING LAB

Payment interfaces and runtime polymorphism

EXPERIMENT 09Java 11+Console program

THE SYLLABUS TASK

What you need to build

Develop a Java program that implements a digital payment system using interfaces and runtime polymorphism. You must create an interface Payment containing void pay(double amount), void refund(double amount) methods. Implement the interface in the following classes with given attributes and appropriate validation logic:
Full syllabus requirements: classes, fields and methods
Develop a Java program that implements a digital payment system using interfaces and runtime polymorphism. You must create an interface Payment containing void pay(double amount), void refund(double amount) methods. Implement the interface in the following classes with given attributes and appropriate validation logic: UPIPayment: upiId, upiPin, balance (Payment allowed only if PIN matches and balance ≥ amount) CardPayment: cardNumber, cardHolder, cvv, limit (Payment allowed only if limit ≥ amount) WalletPayment: walletId, walletBalance (Payment allowed only if wallet balance ≥ amount) All classes must override pay(double amount) to deduct amount and refund() to add back amount. Write a Test Class to create objects for all payment types and display payment success or failure messages accordingly.
Input format & conventions

Three lines: payment amount, fictitious entered UPI PIN, refund amount. Demo balances: UPI 1000 with PIN 1234, card available limit 2000, wallet 500. Successful net payments bound permitted refunds.

UNDERSTAND THE IDEA

Explanation

Payment defines pay and refund contracts. UPI, card and wallet implementations deduct their own available funds, while the driver invokes them through Payment references.

Because pay(amount) has no PIN parameter, UPIPayment exposes a separate demo enterPIN method. Authentication is consumed per payment attempt. A small ledger prevents refunding more than the successful unrefunded payments. All card and PIN values are fictitious; no external payment service is contacted.

TURN THE IDEA INTO STEPS

Step-by-step algorithm

  1. Create the three demonstration payment objects.
  2. Supply the entered PIN to the UPI instance.
  3. Invoke pay through each Payment reference.
  4. Record only successful payments.
  5. Permit positive refunds no larger than netPaid and print final funds.

SEE THE CORE OPERATION

Flowchart

This chart focuses on the named operation or core OOP decision. Diamonds show conditions; labeled branches show outcomes. Read it with the complete algorithm and source for input handling and other operations. Scroll within the diagram on a phone.

Payment and refund state

Payment and refund state

Open full-size flowchart ↗

Java 11+

Complete Java source

Main.java

Download .java
import java.util.*;

public class Main {
    private static final Scanner IN = new Scanner(System.in).useLocale(Locale.ROOT);
    static String text() {
        if (!IN.hasNextLine()) throw new IllegalArgumentException("Incomplete input.");
        String value = IN.nextLine().trim();
        if (value.isEmpty() || value.length() > 200) throw new IllegalArgumentException("Text must have 1–200 characters.");
        return value;
    }
    static int integer(int low, int high) {
        int value = Integer.parseInt(text());
        if (value < low || value > high) throw new IllegalArgumentException("Integer out of range.");
        return value;
    }
    static double number(double low, double high) {
        double value = Double.parseDouble(text());
        if (!Double.isFinite(value) || value < low || value > high) throw new IllegalArgumentException("Number out of range.");
        return value;
    }
    static void money(String label, double value) { System.out.printf(Locale.ROOT, "%s: %.2f%n", label, value); }
interface Payment { void pay(double amount); void refund(double amount); }
    static abstract class Ledger implements Payment {
        private double netPaid;
        static boolean valid(double amount) { return Double.isFinite(amount) && amount>0 && amount<=1000000; }
        void paid(double amount) { netPaid+=amount; }
        boolean refundable(double amount) { return valid(amount) && amount<=netPaid; }
        void refunded(double amount) { netPaid-=amount; }
    }
    static class UPIPayment extends Ledger {
        final String upiId, upiPin; private String enteredPIN=""; double balance;
        UPIPayment(String id,String pin,double balance) { upiId=id; upiPin=pin; this.balance=balance; }
        void enterPIN(String pin) { enteredPIN=pin; }
        @Override public void pay(double amount) {
            boolean authenticated=upiPin.equals(enteredPIN); enteredPIN="";
            if (!valid(amount) || !authenticated || balance<amount) { System.out.println("UPI payment failed."); return; }
            balance-=amount; paid(amount); System.out.println("UPI payment success.");
        }
        @Override public void refund(double amount) {
            if (!refundable(amount)) { System.out.println("UPI refund failed."); return; }
            balance+=amount; refunded(amount); System.out.println("UPI refund success.");
        }
    }
    static class CardPayment extends Ledger {
        final String cardNumber,cardHolder,cvv; double limit;
        CardPayment(String number,String holder,String cvv,double limit) { cardNumber=number;cardHolder=holder;this.cvv=cvv;this.limit=limit; }
        @Override public void pay(double amount) {
            if (!valid(amount) || !cardNumber.matches("[0-9]{16}") || !cvv.matches("[0-9]{3}") || limit<amount) { System.out.println("Card payment failed."); return; }
            limit-=amount; paid(amount); System.out.println("Card payment success.");
        }
        @Override public void refund(double amount) {
            if (!refundable(amount)) { System.out.println("Card refund failed."); return; }
            limit+=amount; refunded(amount); System.out.println("Card refund success.");
        }
    }
    static class WalletPayment extends Ledger {
        final String walletId; double walletBalance;
        WalletPayment(String id,double balance) { walletId=id;walletBalance=balance; }
        @Override public void pay(double amount) {
            if (!valid(amount) || walletBalance<amount) { System.out.println("Wallet payment failed."); return; }
            walletBalance-=amount; paid(amount); System.out.println("Wallet payment success.");
        }
        @Override public void refund(double amount) {
            if (!refundable(amount)) { System.out.println("Wallet refund failed."); return; }
            walletBalance+=amount; refunded(amount); System.out.println("Wallet refund success.");
        }
    }
    // Demo-only: amount, entered UPI PIN, refund amount. Never enter real credentials.
    public static void main(String[] args) {
        try {
            double amount=number(0,1000000); String pin=text(); double refund=number(0,1000000);
            UPIPayment upi=new UPIPayment("demo@example","1234",1000); upi.enterPIN(pin);
            Payment[] methods={upi,new CardPayment("1111222233334444","Demo User","123",2000),new WalletPayment("DEMO-WALLET",500)};
            for (Payment payment:methods) { payment.pay(amount); payment.refund(refund); }
            money("UPI balance",upi.balance); money("Card remaining limit",((CardPayment)methods[1]).limit); money("Wallet balance",((WalletPayment)methods[2]).walletBalance);
        } catch (IllegalArgumentException e) { System.out.println("Invalid input: "+e.getMessage()); }
    }
}

Copy a source file or download the project to preserve its file names.

USE THE RIGHT ENVIRONMENT

How to compile and run

Install a JDK (Java 11 or newer; JDK 17 is suitable), extract this project’s ZIP into its own folder, then open a terminal in that folder.

javac -encoding UTF-8 Main.java
java Main

The standalone source is named Main.java and its entry class is Main. You may also paste it into an online Java compiler and supply the sample input there. Choose Java explicitly; these pages do not change the site compiler or import code automatically.

Each experiment has its own folder because many use the public class Main. Compile and run one experiment at a time.

UNDERSTAND THE JAVA CODE

Line-by-line explanation

These steps follow the exact Java source above. Related statements are grouped; line numbers count blank lines and restart for each file. Start with main to follow execution, then open the constructors, methods or event handlers it uses.

Start with main →

Main.java

Lines 1–4Import utilities and create the shared input reader
import java.util.*;

public class Main {
    private static final Scanner IN = new Scanner(System.in).useLocale(Locale.ROOT);

java.util.* imports utility type names such as Scanner, Locale and collections. Main is the public class matching Main.java. The private static final IN field holds one shared Scanner for System.in; final prevents replacing that reference. The scanner uses Locale.ROOT.

Lines 5–10Read one nonempty trimmed input line
    static String text() {
        if (!IN.hasNextLine()) throw new IllegalArgumentException("Incomplete input.");
        String value = IN.nextLine().trim();
        if (value.isEmpty() || value.length() > 200) throw new IllegalArgumentException("Text must have 1–200 characters.");
        return value;
    }

text checks whether a line exists, reads it and removes leading/trailing whitespace. Reject empty text and lengths above 200 by throwing IllegalArgumentException. Otherwise return the string. One field is read per line, so these helpers do not mix nextInt with leftover newlines.

Lines 11–15Parse and range-check an integer field
    static int integer(int low, int high) {
        int value = Integer.parseInt(text());
        if (value < low || value > high) throw new IllegalArgumentException("Integer out of range.");
        return value;
    }

Integer.parseInt converts the returned string to int or throws NumberFormatException, a subtype of IllegalArgumentException. Reject values outside the caller’s inclusive low/high limits, then return the accepted integer.

Lines 16–20Parse and range-check a finite decimal field
    static double number(double low, double high) {
        double value = Double.parseDouble(text());
        if (!Double.isFinite(value) || value < low || value > high) throw new IllegalArgumentException("Number out of range.");
        return value;
    }

Double.parseDouble reads the string as a double. Double.isFinite rejects NaN and infinities as well as the explicit low/high range check. Return only a finite accepted value; exceptions transfer control to a matching catch block.

Line 21Format a labelled numeric result consistently
    static void money(String label, double value) { System.out.printf(Locale.ROOT, "%s: %.2f%n", label, value); }

money is a formatting helper: %s prints the label, %.2f prints two decimal places, and %n ends the line. Locale.ROOT fixes locale-sensitive formatting. The helper can also display non-currency quantities; it does not change the stored value.

Lines 22–25Define the payment interface and shared ledger rules
interface Payment { void pay(double amount); void refund(double amount); }
    static abstract class Ledger implements Payment {
        private double netPaid;
        static boolean valid(double amount) { return Double.isFinite(amount) && amount>0 && amount<=1000000; }

Payment declares pay and refund. Ledger is abstract and keeps netPaid private. Its valid method accepts only positive finite amounts up to the example limit. Concrete payment types must provide the public interface methods.

Lines 26–29Track what can still be refunded
        void paid(double amount) { netPaid+=amount; }
        boolean refundable(double amount) { return valid(amount) && amount<=netPaid; }
        void refunded(double amount) { netPaid-=amount; }
    }

paid adds a successful payment to netPaid. refundable checks the amount and prevents refunding more than that net total. refunded subtracts only after a refund succeeds. This is per-object in-memory bookkeeping.

Lines 30–33Store UPI demonstration state
    static class UPIPayment extends Ledger {
        final String upiId, upiPin; private String enteredPIN=""; double balance;
        UPIPayment(String id,String pin,double balance) { upiId=id; upiPin=pin; this.balance=balance; }
        void enterPIN(String pin) { enteredPIN=pin; }

UPIPayment inherits ledger helpers and stores its identity, example PIN and balance. enterPIN sets the PIN supplied for the next attempted payment. These are demonstration values, not a real payment gateway connection.

Lines 34–38Authenticate one UPI payment before changing balances
        @Override public void pay(double amount) {
            boolean authenticated=upiPin.equals(enteredPIN); enteredPIN="";
            if (!valid(amount) || !authenticated || balance<amount) { System.out.println("UPI payment failed."); return; }
            balance-=amount; paid(amount); System.out.println("UPI payment success.");
        }

Compare strings with equals, then clear enteredPIN so it is not reused automatically. Reject a bad amount, wrong PIN or insufficient balance. Only success deducts balance and records paid(amount).

Lines 39–43Refund only the permitted UPI amount
        @Override public void refund(double amount) {
            if (!refundable(amount)) { System.out.println("UPI refund failed."); return; }
            balance+=amount; refunded(amount); System.out.println("UPI refund success.");
        }
    }

Check the ledger before changing anything. On success credit balance and reduce netPaid. A failed payment does not create refundable funds.

Lines 44–50Implement the card-specific payment checks
    static class CardPayment extends Ledger {
        final String cardNumber,cardHolder,cvv; double limit;
        CardPayment(String number,String holder,String cvv,double limit) { cardNumber=number;cardHolder=holder;this.cvv=cvv;this.limit=limit; }
        @Override public void pay(double amount) {
            if (!valid(amount) || !cardNumber.matches("[0-9]{16}") || !cvv.matches("[0-9]{3}") || limit<amount) { System.out.println("Card payment failed."); return; }
            limit-=amount; paid(amount); System.out.println("Card payment success.");
        }

The card subtype stores a remaining limit and checks the example number/CVV formats as well as amount and available limit. A successful simulated payment reduces limit and records its refundable amount.

Lines 51–55Restore card limit after an accepted refund
        @Override public void refund(double amount) {
            if (!refundable(amount)) { System.out.println("Card refund failed."); return; }
            limit+=amount; refunded(amount); System.out.println("Card refund success.");
        }
    }

Require refundable(amount), then add to limit and subtract from the ledger. Inherited validation keeps the refund total from exceeding previously successful payments.

Lines 56–62Implement wallet payment using its own balance
    static class WalletPayment extends Ledger {
        final String walletId; double walletBalance;
        WalletPayment(String id,double balance) { walletId=id;walletBalance=balance; }
        @Override public void pay(double amount) {
            if (!valid(amount) || walletBalance<amount) { System.out.println("Wallet payment failed."); return; }
            walletBalance-=amount; paid(amount); System.out.println("Wallet payment success.");
        }

WalletPayment stores its own identity and balance. Reject invalid or unaffordable payments before changing the balance; otherwise deduct and record the payment.

Lines 63–67Implement wallet refunds using the shared ledger
        @Override public void refund(double amount) {
            if (!refundable(amount)) { System.out.println("Wallet refund failed."); return; }
            walletBalance+=amount; refunded(amount); System.out.println("Wallet refund success.");
        }
    }

An accepted refund credits walletBalance and lowers netPaid. The interface is shared with UPI/card, but each subtype applies changes to its own representation.

Lines 68–74Invoke payment and refund through interface references
    // Demo-only: amount, entered UPI PIN, refund amount. Never enter real credentials.
    public static void main(String[] args) {
        try {
            double amount=number(0,1000000); String pin=text(); double refund=number(0,1000000);
            UPIPayment upi=new UPIPayment("demo@example","1234",1000); upi.enterPIN(pin);
            Payment[] methods={upi,new CardPayment("1111222233334444","Demo User","123",2000),new WalletPayment("DEMO-WALLET",500)};
            for (Payment payment:methods) { payment.pay(amount); payment.refund(refund); }

The input comment describes demo-only amount/PIN/refund fields. Construct three example objects, then call pay and refund on Payment references. Runtime dispatch selects each object’s implementation.

Lines 75–78Display subtype balances and handle input errors
            money("UPI balance",upi.balance); money("Card remaining limit",((CardPayment)methods[1]).limit); money("Wallet balance",((WalletPayment)methods[2]).walletBalance);
        } catch (IllegalArgumentException e) { System.out.println("Invalid input: "+e.getMessage()); }
    }
}

The casts recover card and wallet subtype references for their demonstration fields. Print final balances/limits. The catch handles input-helper errors, while business-method rejection messages are printed inside those methods.

FOLLOW THE STATE

Dry run

  1. Paying 200 with PIN 1234 succeeds for all three methods.
  2. Refunding 50 leaves each method with net payment 150.
  3. UPI balance becomes 850, card limit 1850 and wallet balance 350.
  4. An excessive refund is rejected without adding funds.

CHECK THE BEHAVIOUR

Sample input & output

These outputs were captured by running the exact delivered Java project.

Sample 1

INPUT
200
1234
50
OUTPUT
UPI payment success.
UPI refund success.
Card payment success.
Card refund success.
Wallet payment success.
Wallet refund success.
UPI balance: 850.00
Card remaining limit: 1850.00
Wallet balance: 350.00

Sample 2

INPUT
600
0000
1000
OUTPUT
UPI payment failed.
UPI refund failed.
Card payment success.
Card refund failed.
Wallet payment failed.
Wallet refund failed.
UPI balance: 1000.00
Card remaining limit: 1400.00
Wallet balance: 500.00

Common mistakes

  • Do not grant a refund after an unsuccessful payment.
  • Use public implementations of interface methods.
  • Do not alter real website payment files for this lab.

WHY THIS GROWTH RATE?

Time and space complexity

O(p) bounded validation/state work for p payment methods; fixed-size credentials in this example.

Each attempt checks a constant number of numeric conditions and short fixed-format credential strings. The driver processes p methods, giving O(p) work for those accepted bounded fields.

The ledger stores one net-paid total per method. It models state transitions, not network calls or real payment security.

These bounds describe the implemented data processing. Input/output text length, scheduling, framework rendering and runtime/library internals are identified separately where they matter. Educational limits bound the actual examples.

PREPARE FOR YOUR LAB VIVA

Related viva questions & answers

6 focused questions

Try answering aloud, then expand to check your reasoning. Questions focus on this program’s classes, state and behavior.

Why must the implementing pay and refund methods be public?

Interface methods are public contracts. An implementation cannot narrow that visibility, so each overriding method is explicitly public.

How does this example provide a PIN without changing pay(double)?

The UPI object receives an entered PIN through enterPIN before the interface call. pay then verifies that state while preserving the required signature.

Why is the entered PIN cleared after an attempt?

It prevents one authentication entry from silently authorizing later payments. A fresh UPI attempt requires a new explicitly supplied demo PIN.

Why does refund check the netPaid ledger?

Adding funds without a successful payment would fabricate balance. The ledger bounds refunds to the amount successfully paid and not yet refunded.

What does the card’s limit mean in this simulation?

It is remaining available spending capacity. Payment reduces it, and a valid refund restores the refunded portion.

How does Payment[] demonstrate runtime polymorphism?

The array holds different concrete implementations under one interface type. Each pay/refund call runs the actual object’s implementation without switching on its type.