Back to Generics & Type Safety
Q2Question 2 of 7

What are bounded types, wildcards, and PECS?

Key Concepts
  • Bounded type parameters (<T extends Number>) restrict T to a specific type or its subclasses, enabling calls to methods on the bound.
  • Upper-bounded wildcard (? extends T) means the collection produces elements of type T or its subclasses: you can read but not write (except null).
  • Lower-bounded wildcard (? super T) means the collection consumes elements of type T or its superclasses: you can write T but reads return Object.
  • PECS (Producer Extends, Consumer Super) is the rule for choosing the right wildcard: extends for reading, super for writing.
  • Unbounded wildcard (?) means unknown type: List<?> is read-only (elements come out as Object), useful for methods that do not care about element type.
  • Multiple bounds are possible: <T extends Comparable<T> & Serializable> means T must implement both interfaces.
  • The JDK uses PECS throughout: Collections.copy(List<? super T> dest, List<? extends T> src).

Definition

Bounded types restrict type parameters to a specific hierarchy, wildcards add flexibility with ? extends (producer/read) and ? super (consumer/write), and PECS is the rule that ties them together: Producer Extends, Consumer Super.
  • Bounded type parameter – restricts T to Number or its subclasses (Integer, Double, BigDecimal), allowing calls to Number methods like doubleValue()
  • Upper-bounded wildcard – ? extends Account means "some unknown subtype of Account"; you can read elements as Account but cannot add elements (the compiler does not know the exact type)
  • Lower-bounded wildcard – ? super SavingsAccount means "some unknown supertype of SavingsAccount"; you can add SavingsAccount but reads return Object
  • PECS – Producer Extends (read from ? extends T), Consumer Super (write to ? super T); the standard guideline from Effective Java
  • Unbounded wildcard – ? means completely unknown type; List is read-only with elements as Object

At National Bank of India, ? extends Account is like a report that contains accounts of some specific subtype (Savings or Current). You can read accounts from the report, but you cannot file a new account into it because you do not know whether it accepts Savings or Current. ? super SavingsAccount is like a filing drawer that accepts at least SavingsAccount (it might also accept any Account or even Object). You can file a SavingsAccount into it, but when you read something out, you only know it is some Object.

Understanding the Concepts

What are bounded types, wildcards, and PECS?

How do bounded type parameters work?

A bounded type parameter restricts the types that can be used as arguments. means T must be Number or a subclass. Inside the method, you can call any method defined on the bound (Number.doubleValue(), Number.intValue()).

JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.List;

public class BoundedTypeDemo {

    // T must extend Number: can call doubleValue()
    static <T extends Number> BigDecimal sumBalances(List<T> amounts) {
        BigDecimal total = BigDecimal.ZERO;
        for (T amount : amounts) {
            total = total.add(BigDecimal.valueOf(amount.doubleValue()));
        }
        return total;
    }

    public static void main(String[] args) {
        List<Integer> intAmounts = List.of(10000, 25000, 50000);
        List<Double> doubleAmounts = List.of(10000.50, 25000.75);

        System.out.println("Int sum: " + sumBalances(intAmounts));
        System.out.println("Double sum: " + sumBalances(doubleAmounts));

        // List<String> would NOT compile:
        // sumBalances(List.of("SAV2001")); // String does not extend Number
    }
}
TEXT
Int sum: 85000
Double sum: 35001.25

How do upper-bounded wildcards (? extends T) work?

A List is a list of some unknown subtype of Account. You can read elements from it as Account, but you cannot add elements because the compiler does not know the exact subtype.

JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.List;

public class UpperBoundDemo {

    static abstract class Account {
        abstract BigDecimal getBalance();
    }

    static class SavingsAccount extends Account {
        private final BigDecimal balance;
        SavingsAccount(BigDecimal balance) { this.balance = balance; }
        @Override BigDecimal getBalance() { return balance; }
    }

    static class CurrentAccount extends Account {
        private final BigDecimal balance;
        CurrentAccount(BigDecimal balance) { this.balance = balance; }
        @Override BigDecimal getBalance() { return balance; }
    }

    // ? extends Account: PRODUCER (read Account from the list)
    static BigDecimal totalBalance(List<? extends Account> accounts) {
        BigDecimal total = BigDecimal.ZERO;
        for (Account acc : accounts) { // read as Account: OK
            total = total.add(acc.getBalance());
        }
        // accounts.add(new SavingsAccount(...)); // COMPILE ERROR: cannot write
        return total;
    }

    public static void main(String[] args) {
        List<SavingsAccount> savings = List.of(
            new SavingsAccount(new BigDecimal("500000")),
            new SavingsAccount(new BigDecimal("300000"))
        );
        List<CurrentAccount> current = List.of(
            new CurrentAccount(new BigDecimal("1200000"))
        );

        System.out.println("Savings total: " + totalBalance(savings));
        System.out.println("Current total: " + totalBalance(current));
    }
}
TEXT
Savings total: 800000
Current total: 1200000

How do lower-bounded wildcards (? super T) work?

A List is a list of some unknown supertype of SavingsAccount. You can add SavingsAccount to it, but when you read elements, the compiler only knows they are Object.

JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class LowerBoundDemo {

    static class Account {
        String id;
        Account(String id) { this.id = id; }
        @Override public String toString() { return "Account(" + id + ")"; }
    }

    static class SavingsAccount extends Account {
        SavingsAccount(String id) { super(id); }
        @Override public String toString() { return "Savings(" + id + ")"; }
    }

    // ? super SavingsAccount: CONSUMER (write SavingsAccount into the list)
    static void addDefaults(List<? super SavingsAccount> list) {
        list.add(new SavingsAccount("SAV-DEFAULT-001"));
        list.add(new SavingsAccount("SAV-DEFAULT-002"));
        // Reading: can only get Object
        Object obj = list.get(0); // Object, not SavingsAccount
    }

    public static void main(String[] args) {
        List<Account> accountList = new ArrayList<>();
        addDefaults(accountList); // List<Account> is a supertype of SavingsAccount
        System.out.println("Account list: " + accountList);

        List<Object> objectList = new ArrayList<>();
        addDefaults(objectList); // List<Object> is also a supertype
        System.out.println("Object list: " + objectList);
    }
}
TEXT
Account list: [Savings(SAV-DEFAULT-001), Savings(SAV-DEFAULT-002)]
Object list: [Savings(SAV-DEFAULT-001), Savings(SAV-DEFAULT-002)]

What is the PECS rule?

PECS stands for Producer Extends, Consumer Super. It tells you which wildcard to use based on whether you are reading from or writing to the collection.

| Role | Wildcard | Operations | Example |
|---|---|---|---|
| Producer (read) | ? extends T | Read T from it | totalBalance(List) |
| Consumer (write) | ? super T | Write T into it | addDefaults(List) |
| Both read and write | T (no wildcard) | Read and write T | void process(List list) |

The JDK demonstrates PECS in Collections.copy:

JAVA
// JDK signature:
public static <T> void copy(List<? super T> dest,   // CONSUMER: writes T
                             List<? extends T> src)   // PRODUCER: reads T
JAVA
package com.bank.demo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class PecsDemo {
    public static void main(String[] args) {
        List<String> src = List.of("SAV2001", "CUR3001", "FD4001");
        List<String> dest = new ArrayList<>(List.of("", "", ""));

        Collections.copy(dest, src); // dest=consumer(super), src=producer(extends)
        System.out.println("Copied: " + dest);
    }
}
TEXT
Copied: [SAV2001, CUR3001, FD4001]

What are common mistakes?

Mistake: Writing to a ? extends collection

JAVA
// WRONG: cannot add to a producer
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(42); // COMPILE ERROR: ? extends is read-only
JAVA
// CORRECT: use ? super for writing
List<? super Integer> numbers = new ArrayList<Number>();
numbers.add(42); // OK: Integer is a subtype of ? super Integer

Mistake: Reading specific type from ? super

JAVA
// WRONG: cannot read specific type from consumer
List<? super Integer> list = new ArrayList<Number>();
list.add(42);
Integer val = list.get(0); // COMPILE ERROR: can only read as Object
JAVA
// CORRECT: read as Object
Object val = list.get(0); // OK

What about multiple bounds?

A type parameter can have multiple bounds. Use & to require multiple interfaces or a class and interfaces.

JAVA
package com.bank.demo;

import java.io.Serializable;

public class MultipleBoundsDemo {

    // T must be both Comparable and Serializable
    static <T extends Comparable<T> & Serializable> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }

    public static void main(String[] args) {
        String result = max("SAV2001", "CUR3001");
        System.out.println("Max: " + result);

        Integer maxInt = max(500000, 300000);
        System.out.println("Max int: " + maxInt);
    }
}
TEXT
Max: SAV2001
Max int: 500000

What should a senior engineer know?

  • Invariance of Java generics. List is NOT a subtype of List. This is different from arrays where String[] IS a subtype of Object[]. Wildcards compensate for this invariance.
  • Get and Put Principle. This is another name for PECS: use extends when you only get values out of a structure, use super when you only put values into a structure.
  • Intersection types. creates an intersection type. The first bound can be a class; the rest must be interfaces.
  • Type inference improvements. Java 8 improved type inference for method calls in chains. Java 10's var further reduces type declaration verbosity.
  • What are common interview follow-up questions?

    • "What does PECS stand for?" : Producer Extends, Consumer Super. Use extends when reading from a collection, super when writing to it.
    • "Can you add elements to List?" : No. Only null can be added. The compiler does not know the exact subtype.
    • "What is the difference between and ?" : T is a named type parameter you can reference elsewhere in the method. ? is an anonymous wildcard used in a single position.
    • "Give a real example of PECS." : Collections.copy(dest, src) where dest uses ? super T (consumer) and src uses ? extends T (producer).

    Visual Overview

    DIAGRAM
      PECS Rule: Producer Extends, Consumer Super
      =============================================
    
      List<? extends Account>           List<? super SavingsAccount>
      ┌────────────────────────┐        ┌────────────────────────────┐
      │ PRODUCER (read from)    │        │ CONSUMER (write into)       │
      │                         │        │                             │
      │ Account acc = list.get()│        │ list.add(new Savings(...))  │
      │  -> OK (read as Account)│        │  -> OK (write Savings)      │
      │                         │        │                             │
      │ list.add(new Savings()) │        │ Savings s = list.get()      │
      │  -> COMPILE ERROR       │        │  -> COMPILE ERROR           │
      │  (cannot write)         │        │  (can only read as Object)  │
      └────────────────────────┘        └────────────────────────────┘
    
      Collections.copy signature:
      copy(List<? super T> dest,     <-- CONSUMER: write T into dest
           List<? extends T> src)    <-- PRODUCER: read T from src
    

    Key Takeaway

    Bounded types restrict what types can be used as generic arguments. means T must be Number or a subclass, enabling calls to Number methods inside the generic code. Wildcards add flexibility: ? extends T is for reading (producer), ? super T is for writing (consumer), and ? is for methods that do not care about the element type.

    PECS (Producer Extends, Consumer Super) is the essential rule from Effective Java for choosing the right wildcard. When a method reads elements from a collection, declare it as ? extends T so it can accept List, List, or List. When a method writes elements into a collection, declare it as ? super T so it can accept List, List, or any supertype.

    At National Bank of India, Rajesh applied PECS when building the balance calculation service. The method totalBalance(List accounts) could accept a list of any Account subtype, while addDefaults(List list) could write defaults into any list that accepted SavingsAccount. This maximised code reuse while maintaining full type safety.

    Remember one sentence: use ? extends T to read from a collection (Producer Extends) and ? super T to write into a collection (Consumer Super).

    ---

    Share
    Use ← → arrow keys to navigate