Loading learning content...
We've established that a class is a blueprint and an object is an instance. But one of the most powerful aspects of this relationship is often underappreciated: from a single class definition, you can create unlimited objects, each with its own independent state.
This capability is not a minor convenience—it's fundamental to how object-oriented systems model reality. A banking application doesn't have a different class for each customer; it has one Customer class and millions of customer objects. An e-commerce platform doesn't define a new class for each product; it has one Product class and thousands of product instances.
Understanding how to leverage this one-to-many relationship is essential for designing scalable, maintainable systems.
By the end of this page, you will understand how to create and manage multiple objects from a single class, how objects maintain independent state while sharing behavior, common patterns for working with object collections, and practical applications of this capability in real-world systems.
Consider a class that represents a user in a system. From this single definition, we can create as many user objects as needed:
The mechanics:
User class is written once in source codenew User(...) call creates a fresh objectWhat is shared vs what is separate:
| Shared (Per-Class) | Separate (Per-Object) |
|---|---|
| Method implementations | Attribute values |
| Static fields | Instance fields |
| Type information | Object identity |
| Constant values | Current state |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
public class User { // Static: shared across ALL users (one copy) private static int totalUsers = 0; // Instance: separate for EACH user (one copy per object) private String username; private String email; private LocalDateTime createdAt; private int loginCount; public User(String username, String email) { this.username = username; this.email = email; this.createdAt = LocalDateTime.now(); this.loginCount = 0; // Increment the shared counter totalUsers++; } public void login() { this.loginCount++; // Only affects THIS user } public static int getTotalUsers() { return totalUsers; // Shared across all } @Override public String toString() { return username + " (logins: " + loginCount + ")"; }} public class Main { public static void main(String[] args) { // Create multiple users from ONE class definition User alice = new User("alice", "alice@example.com"); User bob = new User("bob", "bob@example.com"); User charlie = new User("charlie", "charlie@example.com"); // Each has independent state alice.login(); alice.login(); bob.login(); System.out.println(alice); // alice (logins: 2) System.out.println(bob); // bob (logins: 1) System.out.println(charlie); // charlie (logins: 0) // But they SHARE static data System.out.println("Total users: " + User.getTotalUsers()); // 3 }}This is the fundamental economics of OOP: you pay the cost of defining behavior ONCE (writing the class) and get to use that behavior MANY times (creating objects). A well-designed class pays dividends across every instance.
A critical property of objects is state independence: changing one object's state has no effect on other objects of the same class (unless they share references to common objects).
Why state independence matters:
Isolation — Objects can be reasoned about individually. You don't need to think about all User objects when debugging one user's behavior.
Concurrency — Independent objects can be processed in parallel without synchronization (as long as they don't share mutable state).
Testing — You can test one object without worrying about side effects on others.
Scalability — Adding more objects doesn't increase the complexity of existing objects.
The guarantee:
When you instantiate two objects from the same class, you get two completely separate containers of data. Modifying one never affects the other, because they occupy different memory regions.
123456789101112131415161718192021222324252627282930313233343536373839404142
public class Counter { private int count; public Counter() { this.count = 0; } public void increment() { this.count++; } public int getCount() { return this.count; }} public class IndependenceDemo { public static void main(String[] args) { // Create three independent counters Counter counter1 = new Counter(); Counter counter2 = new Counter(); Counter counter3 = new Counter(); // Modify each independently counter1.increment(); counter1.increment(); counter1.increment(); counter2.increment(); // counter3 remains at 0 // Each has its own state System.out.println(counter1.getCount()); // 3 System.out.println(counter2.getCount()); // 1 System.out.println(counter3.getCount()); // 0 // PROOF: they are different objects System.out.println(counter1 == counter2); // false System.out.println(counter2 == counter3); // false }}State independence can break when objects share references to the same mutable object. If two User objects both reference the same Address object, changes to that address affect both users. This is sometimes intentional (shared state) and sometimes a bug (accidental aliasing).
Shared references example:
Address sharedAddress = new Address("123 Main St");
User user1 = new User("Alice", sharedAddress);
User user2 = new User("Bob", sharedAddress);
sharedAddress.setStreet("456 Oak Ave");
// Both user1 and user2 now have the new address!
This is intentional shared state when users genuinely share an address (roommates). It's a bug when you meant for each user to have their own address. Understanding this distinction is crucial for correct design.
When you have many objects from the same class, you typically work with them through collections. Understanding how to effectively use collections of objects is a fundamental skill.
Common collection operations:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
public class Product { private String name; private double price; private String category; public Product(String name, double price, String category) { this.name = name; this.price = price; this.category = category; } // Getters public String getName() { return name; } public double getPrice() { return price; } public String getCategory() { return category; }} public class ProductCatalog { private List<Product> products = new ArrayList<>(); public void add(Product product) { products.add(product); } // ITERATION: Apply operation to each object public void printAll() { for (Product p : products) { System.out.println(p.getName() + ": $" + p.getPrice()); } } // FILTERING: Select objects matching criteria public List<Product> findByCategory(String category) { return products.stream() .filter(p -> p.getCategory().equals(category)) .collect(Collectors.toList()); } // AGGREGATION: Compute summary from objects public double totalValue() { return products.stream() .mapToDouble(Product::getPrice) .sum(); } // LOOKUP: Find specific object public Optional<Product> findByName(String name) { return products.stream() .filter(p -> p.getName().equals(name)) .findFirst(); } // TRANSFORMATION: Derive values public List<String> allProductNames() { return products.stream() .map(Product::getName) .collect(Collectors.toList()); }} public class Main { public static void main(String[] args) { ProductCatalog catalog = new ProductCatalog(); // Add many objects from ONE class catalog.add(new Product("Laptop", 999.99, "Electronics")); catalog.add(new Product("Mouse", 29.99, "Electronics")); catalog.add(new Product("Desk", 249.99, "Furniture")); catalog.add(new Product("Chair", 199.99, "Furniture")); catalog.add(new Product("Monitor", 399.99, "Electronics")); // Work with the collection System.out.println("Electronics:"); catalog.findByCategory("Electronics") .forEach(p -> System.out.println(" - " + p.getName())); System.out.println("Total catalog value: $" + catalog.totalValue()); }}List<Product> instead of raw List for type safety.The ability to create multiple objects from one class is not just a programming convenience—it directly mirrors how real-world systems operate. Let's examine several domains:
E-Commerce Platform:
Product class → thousands of product objectsCustomer class → millions of customer objectsOrder class → tens of millions of order objectsReview class → hundreds of millions of review objectsThe class defines the contract; the objects represent the actual entities in the system.
Banking System:
Account class → millions of savings/checking accountsTransaction class → billions of individual transactionsStatement class → millions of monthly statementsHospital Management:
Patient class → thousands of patient recordsAppointment class → hundreds of thousands of scheduled visitsPrescription class → millions of medication ordersDoctor class → hundreds of medical professionals| Domain | Class Example | Typical Object Count | Key Challenge |
|---|---|---|---|
| Social Media | Post | Billions | Efficient storage & retrieval |
| E-Commerce | Product | Millions | Search & recommendation |
| Gaming | Player | Millions | Concurrent access |
| Finance | Transaction | Billions/day | ACID compliance |
| IoT | SensorReading | Trillions | Time-series storage |
In many applications, objects are created from database records, used during a request/session, then discarded. The class defines how to work with the data; the database persists the data between sessions. One class definition can represent millions of database rows.
The modeling paradigm:
This one-to-many relationship enables a powerful approach to software design:
Identify entities — What are the "things" in your domain? (Users, Products, Orders, etc.)
Define classes — Write one class for each entity type, capturing its attributes and behaviors.
Instantiate as needed — Create objects during runtime as entities are created in the real world.
Process uniformly — Apply the same logic to thousands of objects, confident they all respond consistently.
This is why OOP is so effective for business applications—it naturally mirrors the structure of business domains.
Several common design patterns specifically leverage the ability to create many objects from one class:
1. Object Pool Pattern:
Pre-create and reuse objects to avoid instantiation overhead. Useful for expensive-to-create objects (database connections, threads).
class ConnectionPool {
private List<Connection> available = new ArrayList<>();
public Connection acquire() {
if (available.isEmpty()) {
return new Connection(); // Create new if needed
}
return available.remove(0); // Reuse existing
}
public void release(Connection conn) {
available.add(conn); // Return for reuse
}
}
2. Flyweight Pattern:
Share common state across many objects to reduce memory usage. Each object appears unique but internally shares immutable data.
3. Prototype Pattern:
Create new objects by cloning existing ones. One template object serves as the basis for many copies.
4. Factory Pattern:
Encapsulate object creation logic. One factory class produces many product objects.
12345678910111213141516171819202122232425262728293031323334353637383940414243
// One class, many instances via factorypublic class NotificationFactory { public static Notification createEmail(String to, String subject, String body) { Notification n = new Notification(); n.setType(NotificationType.EMAIL); n.setRecipient(to); n.setSubject(subject); n.setBody(body); n.setCreatedAt(Instant.now()); return n; } public static Notification createSMS(String phone, String message) { Notification n = new Notification(); n.setType(NotificationType.SMS); n.setRecipient(phone); n.setBody(message); n.setCreatedAt(Instant.now()); return n; } public static Notification createPush(String deviceToken, String title, String body) { Notification n = new Notification(); n.setType(NotificationType.PUSH); n.setRecipient(deviceToken); n.setSubject(title); n.setBody(body); n.setCreatedAt(Instant.now()); return n; }} // Usage: create many Notification objects through factoryList<Notification> notifications = new ArrayList<>();notifications.add(NotificationFactory.createEmail("user@example.com", "Welcome!", "..."));notifications.add(NotificationFactory.createSMS("+1234567890", "Your code is 1234"));notifications.add(NotificationFactory.createPush("device_token_123", "New message", "...")); // Process all uniformlyfor (Notification n : notifications) { notificationService.send(n);}Many "Gang of Four" design patterns are specifically about managing the creation of multiple objects. Factory, Abstract Factory, Builder, Prototype, and Singleton all address the question: "How do we control and organize the creation of objects?"
While creating objects is relatively cheap in modern runtimes, creating millions of objects has real performance implications. Understanding these helps you make informed design decisions.
Memory footprint:
Each object consumes:
For a simple class with 3 fields:
class Point {
double x; // 8 bytes
double y; // 8 bytes
String label; // 4-8 bytes (reference)
}
// Total: ~40-48 bytes per object (with header and padding)
1 million Point objects ≈ 40-48 MB of heap memory.
Instantiation cost:
TIntArrayList) to avoid boxing overhead.Don't optimize object creation prematurely. Modern garbage collectors are highly efficient. Profile your application first to identify actual bottlenecks. Often, the clarity of having proper objects outweighs the performance of manual optimizations.
We've explored one of the most powerful capabilities of OOP—creating unlimited objects from a single class definition. Let's consolidate:
Module Complete:
You have now mastered the fundamental concepts of classes and objects:
These concepts are the building blocks upon which all object-oriented design rests. Every design pattern, every architectural decision, every principle like SOLID begins here.
Congratulations! You now have a deep understanding of classes and objects—the fundamental building blocks of OOP. This knowledge will serve you throughout your journey into design patterns, SOLID principles, and system design. The next module explores how to design classes with appropriate attributes and methods.