Why Can't I Do new List<>()? Understanding Interfaces vs Concrete Classes
Ever seen List<Set<Integer>> rowSets = new ArrayList<>() and wondered "Why not new List<Set<Integer>>()?" This fundamental question about interfaces vs concrete classes trips up many Java learners. Let's clear it up through a real conversation.
The Question That Started It Allβ
I was looking at some Sudoku solver code and saw this line:
List<Set<Integer>> rowSets = new ArrayList<>();
My immediate thought: "How come we didn't make it like new List<Set<Integer>>()? I guess it is because List<Set<Integer>> is an interface? Or List is an interface and we can't create objects from interfaces. It has something to do with this, right?"
The answer: Exactly right! I've got it.
Let's break down why this works the way it does.
The Simple Answerβ
Yes, you're exactly right! List is an interface, and you cannot create objects from interfaces.
// β Won't work - List is an interface
List<Set<Integer>> rowSets = new List<Set<Integer>>();
// ^^^^^^^^^^^^^^^^^^^
// Compile error: Cannot instantiate the type List
// β
Works - ArrayList is a concrete class that implements List
List<Set<Integer>> rowSets = new ArrayList<Set<Integer>>();
// β
Even better (Java 7+) - Using diamond operator
List<Set<Integer>> rowSets = new ArrayList<>();
Why? Interfaces are contracts/blueprintsβthey define WHAT methods must exist, but not HOW they work. You need a concrete class that actually implements those methods.
Understanding the Piecesβ
What is an Interface?β
Think of an interface as a contract or blueprint:
public interface List<E> {
boolean add(E element);
E get(int index);
int size();
boolean remove(Object o);
// ... and many more methods
}
This says: "Any class that implements List MUST provide these methods."
But notice: The interface doesn't say HOW to implement them!
- Where will the data be stored?
- How will
add()actually work? - What data structure will be used internally?
The interface doesn't know! It just defines the contract.
What is a Concrete Class?β
A concrete class is the actual implementation of the interface contract:
public class ArrayList<E> implements List<E> {
// HOW: Store elements in an array
private Object[] elements;
private int size;
@Override
public boolean add(E element) {
// HOW: Add to the internal array
if (size >= elements.length) {
// Grow the array if needed
elements = Arrays.copyOf(elements, elements.length * 2);
}
elements[size++] = element;
return true;
}
@Override
public E get(int index) {
// HOW: Get from the internal array
if (index >= size) {
throw new IndexOutOfBoundsException();
}
return (E) elements[index];
}
@Override
public int size() {
// HOW: Return the current size
return size;
}
// ... all other List methods implemented
}
Notice: ArrayList provides the HOWβit uses an internal array and actually implements all the methods!
Wait... Did We Just Make Up List<Set<Integer>>?β
Another great question popped up: "But how does the Java compiler know there is a List<Set<Integer>> kind of interface? We just made it up?"
Short answer: No, we didn't make it up! List is a real interface in Java's standard library.
Where List Comes Fromβ
List is defined in Java's standard library (java.util.List):
package java.util;
public interface List<E> {
void add(E element);
E get(int index);
int size();
boolean remove(Object o);
// ... more methods
}
ArrayList implements that interface:
package java.util;
public class ArrayList<E> implements List<E> {
@Override
public void add(E element) {
// Actual implementation using an internal array
}
@Override
public E get(int index) {
// Actual implementation
}
@Override
public int size() {
// Actual implementation
}
// ... more implementations
}
How the Compiler Validates Your Codeβ
When you write:
List<Set<Integer>> rowSets = new ArrayList<Set<Integer>>();
The compiler goes through these steps:
- Looks up
Listin thejava.utilpackage β finds the interface β - Looks up
ArrayListin thejava.utilpackage β finds the class β - Verifies that
ArrayListimplementsListβ β yes it does - Checks the generic types match β
List<Set<Integer>>andArrayList<Set<Integer>>β β yes they do - Allows the assignment β
Where Does Java Know About These?β
They're part of the Java Standard Library (built-in). When you import them:
import java.util.List;
import java.util.ArrayList;
The compiler can find the actual interface and class definitions.
Without the import, the compiler would complain:
// No import statements
public class Test {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
// Error: cannot find symbol
// symbol: class List
// location: class Test
}
}
With the import, the compiler knows where to find them:
import java.util.List;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
List<String> names = new ArrayList<>(); // β
Works!
}
}
The Generic Type Magicβ
The <E> in List<E> is a generic type parameter:
public interface List<E> { // E is a placeholder
void add(E element);
E get(int index);
}
When you write List<Set<Integer>>:
- Java replaces
EwithSet<Integer> - So
void add(E element)becomesvoid add(Set<Integer> element) - And
E get(int index)becomesSet<Integer> get(int index)
It's not made upβit's Java's type system in action!
The Restaurant Analogyβ
Interface = The Menuβ
public interface Menu {
Dish serveBreakfast();
Dish serveLunch();
Dish serveDinner();
}
The menu tells you:
- β What dishes are available
- β What you can order
- β But NOT how to cook them!
You can't eat the menu! You need an actual chef who knows how to make the food.
Concrete Class = The Chefβ
public class Chef implements Menu {
@Override
public Dish serveBreakfast() {
// Chef knows HOW to make breakfast
Eggs eggs = cookEggs();
Bacon bacon = fryBacon();
return new Dish(eggs, bacon);
}
@Override
public Dish serveLunch() {
// Chef knows HOW to make lunch
Bread bread = toastBread();
Ham ham = sliceHam();
return new Sandwich(bread, ham);
}
@Override
public Dish serveDinner() {
// Chef knows HOW to make dinner
Steak steak = grillSteak();
Potatoes potatoes = mashPotatoes();
return new Dish(steak, potatoes);
}
}
The chef provides the HOW!
In Java terms:
// β Can't do this - you can't eat the menu!
Menu meal = new Menu();
// β
This works - the chef can make the food!
Menu meal = new Chef();
Why Declare as Interface But Create as Concrete Class?β
The Pattern Explainedβ
InterfaceType variable = new ConcreteImplementationClass<>();
Left side (variable type): What capabilities you need Right side (object creation): How those capabilities are implemented
Example 1: Collectionsβ
// Declare as List (interface)
List<String> names = new ArrayList<>();
// ^^^^ ^^^^^^^^^
// WHAT HOW
What this means:
- "I need something that acts like a List"
- "I don't care HOW it's implemented"
- "Just give me add(), get(), size(), etc."
Why this is powerful:
// Later, you can easily switch implementations:
List<String> names = new LinkedList<>(); // Different HOW, same WHAT
// Your code using 'names' doesn't change!
names.add("Alice");
names.get(0);
names.size();
Example 2: Your Sudoku Codeβ
List<Set<Integer>> rowSets = new ArrayList<>();
Breaking it down:
Set<Integer>- Each element is a Set of Integers (interface)List<Set<Integer>>- A List of those Sets (interface)new ArrayList<>()- Create an ArrayList (concrete implementation)
Visual representation:
rowSets (List interface)
β
[HashSet{}, HashSet{}, HashSet{}, ...] (ArrayList implementation)
β β β
Set Set Set (interface)
Common List Implementationsβ
Java provides multiple concrete implementations of the List interface:
ArrayList - Array-Basedβ
List<String> names = new ArrayList<>();
How it works:
- Uses an internal array
- Fast random access:
get(index)is O(1) - Slow insertions in middle: O(n)
- Good for: Lots of reading, few insertions
LinkedList - Linked Nodesβ
List<String> names = new LinkedList<>();
How it works:
- Uses linked nodes (each element points to next)
- Slow random access:
get(index)is O(n) - Fast insertions anywhere: O(1)
- Good for: Lots of insertions, few random accesses
Vector - Thread-Safe ArrayListβ
List<String> names = new Vector<>();
How it works:
- Like ArrayList but synchronized
- Thread-safe but slower
- Good for: Multi-threaded environments (rarely used today)
The Same Pattern Everywhereβ
This "interface on left, concrete on right" pattern appears throughout Java:
Mapsβ
// β Can't do this - Map is an interface
Map<String, Integer> scores = new Map<>();
// β
Works - HashMap is concrete
Map<String, Integer> scores = new HashMap<>();
// β
Or TreeMap, LinkedHashMap, etc.
Map<String, Integer> scores = new TreeMap<>();
Setsβ
// β Can't do this - Set is an interface
Set<Integer> numbers = new Set<>();
// β
Works - HashSet is concrete
Set<Integer> numbers = new HashSet<>();
// β
Or TreeSet, LinkedHashSet, etc.
Set<Integer> numbers = new TreeSet<>();
Collections Summary Tableβ
| Interface | Common Implementations | Choose Based On |
|---|---|---|
List | ArrayList, LinkedList, Vector | Access pattern, insertion frequency |
Set | HashSet, TreeSet, LinkedHashSet | Ordering needs, uniqueness guarantee |
Map | HashMap, TreeMap, LinkedHashMap | Key ordering, lookup speed |
Why This Design?β
Flexibilityβ
public class UserService {
private List<User> users; // Interface type
public UserService() {
// Easy to change implementation later!
// Small number of users? ArrayList is fine
this.users = new ArrayList<>();
// Lots of insertions? Switch to LinkedList
// this.users = new LinkedList<>();
// Thread-safe needed? Switch to Vector
// this.users = new Vector<>();
}
public void addUser(User user) {
users.add(user); // Works with ANY List implementation!
}
}
The rest of your code doesn't care which implementation you use!
Testabilityβ
public class PaymentService {
private PaymentProcessor processor; // Interface
public PaymentService(PaymentProcessor processor) {
this.processor = processor;
}
public void processPayment(Order order) {
processor.charge(order.getTotal());
}
}
// Production: Real payment processor
PaymentProcessor realProcessor = new StripePaymentProcessor();
PaymentService service = new PaymentService(realProcessor);
// Testing: Mock payment processor
PaymentProcessor mockProcessor = new MockPaymentProcessor();
PaymentService testService = new PaymentService(mockProcessor);
Maintainabilityβ
// You program against the interface (WHAT)
public void printAllNames(List<String> names) {
for (String name : names) {
System.out.println(name);
}
}
// Works with ANY List implementation (HOW)
printAllNames(new ArrayList<>());
printAllNames(new LinkedList<>());
printAllNames(new Vector<>());
Your method doesn't need to change when the implementation changes!
Complete Example: Building a Shopping Cartβ
Step 1: Define the Interface (WHAT)β
public interface ShoppingCart {
void addItem(Product product);
void removeItem(Product product);
double getTotal();
List<Product> getItems();
}
Step 2: Create Concrete Implementations (HOW)β
// Implementation 1: In-Memory Cart
public class InMemoryCart implements ShoppingCart {
private List<Product> items = new ArrayList<>();
@Override
public void addItem(Product product) {
items.add(product);
}
@Override
public void removeItem(Product product) {
items.remove(product);
}
@Override
public double getTotal() {
return items.stream()
.mapToDouble(Product::getPrice)
.sum();
}
@Override
public List<Product> getItems() {
return new ArrayList<>(items);
}
}
// Implementation 2: Database-Backed Cart
public class DatabaseCart implements ShoppingCart {
private Long userId;
private CartRepository repository;
@Override
public void addItem(Product product) {
repository.addItemToCart(userId, product);
}
@Override
public void removeItem(Product product) {
repository.removeItemFromCart(userId, product);
}
@Override
public double getTotal() {
return repository.getCartTotal(userId);
}
@Override
public List<Product> getItems() {
return repository.getCartItems(userId);
}
}
Step 3: Use the Interfaceβ
public class CheckoutService {
public void checkout(ShoppingCart cart, PaymentDetails payment) {
// This works with ANY ShoppingCart implementation!
double total = cart.getTotal();
List<Product> items = cart.getItems();
// Process payment
processPayment(total, payment);
// Ship items
shipItems(items);
}
}
// Usage:
ShoppingCart cart = new InMemoryCart(); // Or DatabaseCart
CheckoutService checkout = new CheckoutService();
checkout.checkout(cart, paymentDetails);
Common Mistakes and How to Avoid Themβ
Mistake 1: Trying to Instantiate an Interfaceβ
// β WRONG - Won't compile
List<String> names = new List<>();
// Error: Cannot instantiate the type List<String>
Fix:
// β
CORRECT - Use a concrete implementation
List<String> names = new ArrayList<>();
Mistake 2: Using Concrete Type Unnecessarilyβ
// β οΈ Less flexible
ArrayList<String> names = new ArrayList<>();
// Now you can't easily switch to LinkedList later!
Better:
// β
More flexible - program against the interface
List<String> names = new ArrayList<>();
// Now you can switch implementations easily:
// List<String> names = new LinkedList<>();
Mistake 3: Forgetting the Diamond Operatorβ
// β οΈ Works but gives warning (raw type)
List<String> names = new ArrayList();
// Warning: ArrayList is a raw type. References to generic type ArrayList<E> should be parameterized
Better:
// β
Type-safe with diamond operator
List<String> names = new ArrayList<>();
Visual Summaryβ
The Relationshipβ
βββββββββββββββββββββββββββββββββββββββ
β Interface (Contract) β
β List<E> β
β - Defines WHAT methods exist β
β - No implementation (HOW) β
β - Cannot be instantiated β
βββββββββββββββββββββββββββββββββββββββ
β²
β implements
β
βββββββββββββ΄ββββββββββββ¬ββββββββββββββββ
β β β
ββββββΌβββββββ ββββββββββΌββββββ ββββββββΌβββββββ
β ArrayList β β LinkedList β β Vector β
β β β β β β
β Concrete β β Concrete β β Concrete β
β Class β β Class β β Class β
β β β β β β
β Provides β β Provides β β Provides β
β HOW β β HOW β β HOW β
β β β β β β
β Can be β β Can be β β Can be β
βcreated β β created β β created β
βββββββββββββ ββββββββββββββββ βββββββββββββββ
The Declaration Patternβ
InterfaceType variable = new ConcreteClass<>();
βββββββ¬ββββββ ββββββββ¬βββββββ
β β
What you How it's
need implemented
Key Takeawaysβ
-
Interfaces are Contracts:
- Define WHAT methods must exist
- Don't define HOW they work
- Cannot be instantiated directly
-
Concrete Classes Provide Implementation:
- Define HOW methods work
- Provide actual functionality
- Can be instantiated
-
The Pattern:
InterfaceType variable = new ConcreteClass<>(); -
Why This Matters:
- Flexibility to change implementations
- Code against contracts, not specifics
- Easy testing with mock implementations
- Better maintainability
-
Common Pairs:
ListβArrayList,LinkedList,VectorSetβHashSet,TreeSet,LinkedHashSetMapβHashMap,TreeMap,LinkedHashMap
Final Mental Modelβ
Think of interfaces and classes like this:
Interface = Job Description
"We need someone who can:
- Cook breakfast
- Cook lunch
- Cook dinner"
Concrete Class = Actual Person
"I'm a chef and I know HOW to:
- Cook breakfast (eggs and bacon)
- Cook lunch (sandwiches)
- Cook dinner (steak and potatoes)"
You can't hire the job descriptionβyou need to hire an actual person!
Similarly, you can't instantiate an interfaceβyou need to create a concrete class!
// β Can't hire the job description
Chef chef = new JobDescription();
// β
Hire an actual chef
Chef chef = new ItalianChef();
// β
Or French chef, Chinese chef, etc.
Chef chef = new FrenchChef();
Wrapping Up: The Key Insightsβ
Let's revisit the original questions:
Q1: "Why not new List<Set<Integer>>()?"β
Answer: Because List is an interfaceβa contract that defines WHAT methods must exist, but not HOW they work. You need a concrete class like ArrayList that provides the actual implementation.
// β Won't work - List is an interface
List<Set<Integer>> rowSets = new List<Set<Integer>>();
// β
Works - ArrayList is a concrete class
List<Set<Integer>> rowSets = new ArrayList<>();
Q2: "Did we just make up List<Set<Integer>>?"β
Answer: No! List is part of Java's standard library (java.util.List). When you import it, the compiler knows where to find the interface definition. The <Set<Integer>> part is just using Java's generics system to specify what type the List will hold.
import java.util.List; // Real interface from Java's library
import java.util.ArrayList; // Real class from Java's library
import java.util.Set; // Real interface from Java's library
List<Set<Integer>> rowSets = new ArrayList<>(); // All real, nothing made up!
Q3: "So the pattern is always Interface = Concrete Class?"β
Exactly! The pattern is:
InterfaceType variable = new ConcreteClass<>();
βββββββ¬ββββββ ββββββββ¬βββββββ
What you How it's
need implemented
Other examples:
Map<String, Integer> scores = new HashMap<>();
Set<Integer> numbers = new HashSet<>();
Collection<String> items = new ArrayList<>();
Final Mental Modelβ
Think of it this way:
- Interface = Job description (WHAT needs to be done)
- Concrete Class = Actual employee (HOW to do it)
You can't hire a job descriptionβyou need to hire an actual person!
Similarly:
- You can't instantiate an interfaceβyou need a concrete class!
- But you can declare the variable type as the interface for flexibility
// Declare as interface (flexibility)
List<String> names = new ArrayList<>();
// Later, easy to switch implementations
names = new LinkedList<>(); // Different HOW, same WHAT
Remember: Once you understand that interfaces are contracts (WHAT) and concrete classes are implementations (HOW), everything clicks into place. List is the job description, ArrayList is the actual employee. You can't create objects from job descriptionsβyou need the real thing! π―
Tags: #java #interfaces #collections #fundamentals #arraylist #type-system
