Loading content...
If attributes give objects their memory, methods give objects their life. Methods are the behaviors, actions, and capabilities that objects possess. They are the verbs to the attributes' nouns—the doing that complements the being.
Consider a real-world bank account. It doesn't just have a balance; it can deposit, withdraw, transfer, and calculate interest. These are behaviors—things the account can do. In code, these behaviors become methods: functions that belong to objects and operate on their state.
By the end of this page, you will understand methods as the mechanism for object behavior, know the different types of methods and when to use each, understand how methods interact with object state, and recognize the principles that guide good method design.
A method is a function that is declared within a class and defines behavior for objects of that class. Unlike standalone functions in procedural programming, methods:
The Technical Definition:
Methods consist of:
12345678910111213141516171819202122
public class BankAccount { private String accountNumber; private double balance; // Method anatomy breakdown: // [access modifier] [return type] [name]([parameters]) { [body] } public double deposit(double amount) { // public: visible everywhere // double: returns a number // deposit: descriptive verb name // amount: parameter needed to perform action if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive"); } this.balance += amount; // Modifies object state return this.balance; // Returns the new balance } // 'this' keyword refers to the current object instance // The same method code works for any BankAccount object, // but operates on that specific object's attributes}The this Reference:
Inside a method, the keyword this refers to the object on which the method was called. When you write account1.deposit(100), within the deposit method, this refers to account1. This is how the same method code can work for millions of different bank account objects—each invocation operates on a different this.
This implicit context is what separates methods from functions. A function receives all its data through parameters. A method receives data through parameters plus the implicit object context.
Methods can be classified by their purpose and relationship to object state. Understanding these categories helps you design classes with intention.
Query vs Command (CQS Principle):
A powerful design principle distinguishes between two fundamental method types:
Query methods answer questions about the object without changing it. Calling a query multiple times has no effect—it's like looking at a clock; looking doesn't change the time.
Command methods tell the object to do something that changes state or produces side effects. Calling a command changes the world.
The Command-Query Separation (CQS) principle suggests that methods should be either queries or commands, but not both. This separation makes code easier to reason about—you know that getters are safe to call without consequences.
12345678910111213141516171819202122232425262728293031323334353637383940414243
public class ShoppingCart { private List<Item> items = new ArrayList<>(); private double totalPrice; // QUERY METHODS - Ask questions, don't change state public int getItemCount() { return items.size(); // Pure query - no side effects } public double getTotalPrice() { return totalPrice; // Returns current state } public boolean isEmpty() { return items.isEmpty(); // Computed query } public Item getMostExpensiveItem() { return items.stream() .max(Comparator.comparing(Item::getPrice)) .orElse(null); // Complex query, still no mutation } // COMMAND METHODS - Change state, should not return queries public void addItem(Item item) { items.add(item); totalPrice += item.getPrice(); // State mutation } public void removeItem(Item item) { if (items.remove(item)) { totalPrice -= item.getPrice(); } } public void clear() { items.clear(); totalPrice = 0; // Reset state } // VIOLATION of CQS - both queries and commands (avoid this) // public Item removeAndReturnMostExpensive() { ... }}Pure CQS isn't always practical. Stack.pop() is a classic example that both modifies state (removes element) and returns a value (the removed element). The principle is a guideline, not a law. When you violate it, do so consciously and document it.
A method's signature is its external interface—what callers need to know to use it correctly. A method's contract goes deeper, specifying the rules and promises that govern its use.
Components of a Signature:
The Method Contract:
Beyond the signature, every method has an implicit or explicit contract consisting of:
Preconditions — What must be true before calling the method
Postconditions — What will be true after the method returns
Invariants — What remains constant
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
public class BankAccount { private double balance; private boolean isActive; /** * Withdraws the specified amount from this account. * * PRECONDITIONS: * - amount > 0 (positive withdrawal) * - amount <= balance (sufficient funds) * - account must be active * * POSTCONDITIONS: * - balance is reduced by amount * - returns the new balance * * INVARIANT: * - balance >= 0 (never negative) * * @param amount the amount to withdraw (must be positive) * @return the new balance after withdrawal * @throws IllegalArgumentException if amount is not positive * @throws IllegalStateException if account is not active * @throws InsufficientFundsException if balance < amount */ public double withdraw(double amount) { // Validate preconditions if (amount <= 0) { throw new IllegalArgumentException("Amount must be positive"); } if (!isActive) { throw new IllegalStateException("Account is not active"); } if (amount > balance) { throw new InsufficientFundsException("Insufficient funds"); } // Execute the operation balance -= amount; // Postcondition is guaranteed: balance reduced by amount // Invariant maintained: balance cannot go negative due to precondition check return balance; }}The concept of preconditions, postconditions, and invariants comes from 'Design by Contract,' pioneered by Bertrand Meyer. When contracts are explicit and enforced, bugs become easier to find because violations are detected early, close to the source.
Just as attributes can be instance or static, methods can too. The distinction is fundamental:
Instance Methods:
account.deposit(100)this to refer to the current objectStatic Methods:
Math.sqrt(16)this (no object context exists)| Aspect | Instance Method | Static Method |
|---|---|---|
| Context | Operates on a specific object | No object context |
| Access | Instance + static members | Static members only |
| Call syntax | object.method() | ClassName.method() |
| 'this' keyword | Available (refers to object) | Not available |
| Polymorphism | Supports overriding | Does not support overriding |
| Use case | Object behavior | Utility, factory, pure functions |
12345678910111213141516171819202122232425262728293031323334
public class Temperature { private double celsius; // Instance attribute // Instance method - works with this object's state public double toCelsius() { return this.celsius; // Accesses instance state } public double toFahrenheit() { return this.celsius * 9/5 + 32; // Computes from instance state } // Static utility methods - pure conversion, no object needed public static double celsiusToFahrenheit(double celsius) { return celsius * 9/5 + 32; // No 'this', no instance state } public static double fahrenheitToCelsius(double fahrenheit) { return (fahrenheit - 32) * 5/9; } // Static factory method - creates objects public static Temperature fromFahrenheit(double fahrenheit) { Temperature temp = new Temperature(); temp.celsius = fahrenheitToCelsius(fahrenheit); return temp; // Returns a new instance }} // Usage:// Instance method: Temperature temp = new Temperature(100);// double f = temp.toFahrenheit();// Static method: double f = Temperature.celsiusToFahrenheit(100);// Factory method: Temperature temp = Temperature.fromFahrenheit(212);Excessive static methods indicate procedural thinking disguised as OOP. If a class is mostly static methods operating on data passed as parameters, ask: should this data be in objects with instance methods instead? Static methods have their place (utilities, factories), but instance methods are the heart of OOP.
Methods communicate with their callers through parameters (input) and return values (output). Designing these interfaces well is critical for usable, maintainable code.
Parameter Design Principles:
123456789101112131415161718192021222324
// POOR: Too many parameters, boolean flag, easy to mix up stringspublic void createUser(String firstName, String lastName, String email, String phone, String address, boolean isAdmin, boolean sendWelcomeEmail, boolean requirePasswordChange) { // Callers will write: createUser("John", "Doe", "email", "phone", "addr", true, false, true) // What does true, false, true mean? Which string is which?} // BETTER: Parameter object + separate methodspublic class UserCreationRequest { private final Name name; // Groups first + last private final ContactInfo contact; // Groups email + phone + address private final UserRole role; // Enum instead of boolean // Builder pattern for clean construction public static Builder builder() { ... }} public void createUser(UserCreationRequest request) { // Single parameter, self-documenting} public void sendWelcomeEmail(User user) { ... } // Separate concernpublic void requirePasswordChange(User user) { ... } // Separate concernReturn Value Design:
this to enable method chaining where appropriateWhen a method needs more than 3-4 parameters, consider creating a parameter object—a simple class that bundles related parameters. This makes calls readable, allows named construction (via builders), and makes it easy to add optional parameters later without changing the signature.
Method visibility determines who can call a method. This is fundamental to encapsulation—hiding implementation details while exposing a clean interface.
The Visibility Levels (Java/C#):
| Modifier | Same Class | Same Package | Subclass | Everywhere |
|---|---|---|---|---|
| private | ✓ | ✗ | ✗ | ✗ |
| (default) | ✓ | ✓ | ✗ | ✗ |
| protected | ✓ | ✓ | ✓ | ✗ |
| public | ✓ | ✓ | ✓ | ✓ |
Design Principle: Minimize Visibility
Start with the most restrictive visibility that works. Make methods private by default and only increase visibility when there's a genuine need.
Why?
1234567891011121314151617181920212223242526272829303132
public class Order { private List<Item> items = new ArrayList<>(); // PUBLIC: Part of the external interface // Callers depend on this; changing it breaks things public void addItem(Item item) { validateItem(item); // Calls private method items.add(item); recalculateTotal(); // Calls private method } // PRIVATE: Implementation detail, not part of interface // Can be refactored, renamed, or removed without breaking callers private void validateItem(Item item) { if (item == null) { throw new IllegalArgumentException("Item cannot be null"); } if (item.getPrice() < 0) { throw new IllegalArgumentException("Item price cannot be negative"); } } // PRIVATE: Internal computation, may change frequently private void recalculateTotal() { // Implementation can change freely } // PROTECTED: Visible to subclasses for customization points protected double calculateDiscount() { return 0.0; // Subclasses can override to implement custom discounts }}Think of public methods as promises you make to the world. Every public method is a commitment. Private methods are implementation details—free to change. The distinction between interface (public) and implementation (private) is fundamental to maintainable software.
The interplay between methods and attributes is the heart of OOP. Methods read, modify, compute from, and protect object state.
Patterns of State Interaction:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
public class Order { // STATE ATTRIBUTES private OrderStatus status = OrderStatus.DRAFT; private List<Item> items = new ArrayList<>(); private double total; private LocalDateTime submittedAt; private LocalDateTime completedAt; // READ STATE - returns current attribute value public OrderStatus getStatus() { return status; } // DERIVED READ - computes from state public int getItemCount() { return items.size(); } // MODIFY STATE - with validation public void addItem(Item item) { guardDraftStatus(); // Can only modify drafts items.add(item); recalculateTotal(); // Maintain consistency } // TRANSITION STATE - move through lifecycle public void submit() { guardDraftStatus(); validateForSubmission(); this.status = OrderStatus.SUBMITTED; this.submittedAt = LocalDateTime.now(); } public void complete() { if (status != OrderStatus.SUBMITTED) { throw new IllegalStateException("Can only complete submitted orders"); } this.status = OrderStatus.COMPLETED; this.completedAt = LocalDateTime.now(); } // GUARD STATE - prevent invalid operations private void guardDraftStatus() { if (status != OrderStatus.DRAFT) { throw new IllegalStateException( "Can only modify orders in DRAFT status, current: " + status ); } } // VALIDATE STATE - ensure consistency private void validateForSubmission() { if (items.isEmpty()) { throw new IllegalStateException("Cannot submit empty order"); } } // MAINTAIN CONSISTENCY - keep derived state accurate private void recalculateTotal() { this.total = items.stream() .mapToDouble(Item::getPrice) .sum(); }}Every time you modify state, ask: does this leave the object in a consistent state? If multiple attributes must change together, they should change atomically within a method. Never leave an object in an inconsistent intermediate state visible to callers.
We've explored methods comprehensively. Let's consolidate the key insights:
What's Next:
Now that we understand both attributes and methods individually, we'll explore how to design them together. The next page covers designing meaningful attributes—how to choose which attributes a class needs and how to ensure they serve the class's purpose well.
You now understand methods as the mechanism for object behavior. You can classify methods by purpose, design clean signatures and contracts, choose appropriate visibility, and reason about how methods interact with state. Next, we'll design meaningful attributes.