Introduction
Variable discarding in Java refers to the practice of intentionally ignoring values returned from methods or created during operations. This is particularly useful when you need to call a method for its side effects but don't care about its return value, or when working with pattern matching where you want to ignore certain components.
Traditional Approaches to Discarding Values
Using Dummy Variables
public class TraditionalDiscarding { public void demonstrateTraditionalApproaches() { // Method returns value but we don't need it String result = someMethodThatReturnsValue(); // 'result' is never used - this creates compiler warnings // Working with Map operations Map<String, String> map = new HashMap<>(); String oldValue = map.put("key", "value"); // Don't care about old value // File operations File file = new File("test.txt"); boolean created = file.createNewFile(); // Only care about side effect // List operations List<String> list = new ArrayList<>(); boolean changed = list.add("item"); // Don't care about return value } private String someMethodThatReturnsValue() { System.out.println("Method executed for side effects"); return "returned value"; } } Java 8+ Discarding Techniques
Using void Methods for Side Effects
public class MethodDiscarding { public void demonstrateMethodDiscarding() { List<String> names = Arrays.asList("John", "Jane", "Doe"); // forEach accepts Consumer - return value is automatically discarded names.forEach(name -> { System.out.println("Processing: " + name); // No return value to worry about }); // Stream operations with side effects names.stream() .filter(name -> name.length() > 3) .forEach(System.out::println); // Value automatically discarded // Optional ifPresent for side effects Optional<String> optionalName = Optional.of("John"); optionalName.ifPresent(name -> System.out.println("Name found: " + name)); } } Java 21+ Pattern Matching Discard
The Underscore (_) Discard Pattern
public class ModernDiscarding { // Pattern matching with switch expressions public String processObject(Object obj) { return switch (obj) { case String s -> "String: " + s; case Integer _ -> "It's an integer (value discarded)"; // Discard the value case Double _ -> "It's a double (value discarded)"; // Discard the value case null -> "Null object"; default -> "Unknown type: " + obj.getClass().getSimpleName(); }; } // Record pattern matching with discards public void demonstrateRecordPatternDiscarding() { record Point(int x, int y) {} record Rectangle(Point topLeft, Point bottomRight) {} Object shape = new Rectangle(new Point(1, 2), new Point(3, 4)); if (shape instanceof Rectangle(Point _, Point _)) { System.out.println("It's a rectangle, but we don't care about coordinates"); } if (shape instanceof Rectangle(Point(int x, _), Point(_, int y))) { System.out.println("Only care about x from first point and y from second: " + x + ", " + y); } } // Multiple discards in complex patterns public void processComplexData() { record Person(String name, int age, String email) {} record Department(String name, Person manager, List<Person> employees) {} Department dept = new Department("Engineering", new Person("Alice", 30, "[email protected]"), List.of(new Person("Bob", 25, "[email protected]"))); // Discard manager details and employee list if (dept instanceof Department(String deptName, Person _, List<Person> _)) { System.out.println("Department: " + deptName); } // Discard only specific fields if (dept instanceof Department(_, Person(String name, _, _), _)) { System.out.println("Manager name: " + name); } } } Lambda Expressions and Discarding
Discarding Lambda Parameters
public class LambdaDiscarding { public void demonstrateLambdaDiscarding() { Map<String, Integer> map = new HashMap<>(); map.put("A", 1); map.put("B", 2); map.put("C", 3); // Traditional approach - use both parameters map.forEach((key, value) -> { System.out.println("Key: " + key + ", Value: " + value); }); // Discard value parameter map.forEach((key, _) -> { System.out.println("Key: " + key); // Value discarded }); // Discard key parameter map.forEach((_, value) -> { System.out.println("Value: " + value); // Key discarded }); // BiConsumer with explicit discarding BiConsumer<String, Integer> keyOnlyPrinter = (key, _) -> System.out.println("Processing key: " + key); map.forEach(keyOnlyPrinter); } // Method references that implicitly discard public void methodReferenceDiscarding() { List<String> names = Arrays.asList("John", null, "Jane", null, "Doe"); // Remove nulls - the boolean return value is discarded names.removeIf(Objects::isNull); // System.out.println returns void, so return value is automatically discarded names.forEach(System.out::println); } } Exception Handling with Discarding
Discarding Exception Information
public class ExceptionDiscarding { public void demonstrateExceptionDiscarding() { // Traditional exception handling - we have to name the exception try { riskyOperation(); } catch (IOException e) { // We have to declare 'e' even if we don't use it System.out.println("IO operation failed"); } // With newer Java features, we can better handle this safeRiskyOperation(); } private void riskyOperation() throws IOException { throw new IOException("Simulated IO error"); } // Method that handles exception internally and discards details private void safeRiskyOperation() { try { riskyOperation(); } catch (IOException _) { // Using _ to indicate we're deliberately ignoring the exception object System.out.println("Operation failed, but we don't need exception details"); // Log the failure but don't need the stack trace for this case } } // Multiple exception types with discarding public void processWithMultipleExceptions() { try { multiRiskyOperation(); } catch (IOException | SQLException _) { // Discard exception objects for both types System.out.println("Database operation failed"); // No need for detailed exception handling in this context } } private void multiRiskyOperation() throws IOException, SQLException { // Simulate different exceptions if (Math.random() > 0.5) { throw new IOException("IO error"); } else { throw new SQLException("SQL error"); } } } Stream API and Discarding
Discarding Stream Elements
public class StreamDiscarding { public void demonstrateStreamDiscarding() { List<String> names = Arrays.asList("John", "Jane", "Doe", "Alice", "Bob"); // peek() for side effects - return value is automatically handled long count = names.stream() .peek(name -> System.out.println("Processing: " + name)) // Value discarded .filter(name -> name.length() > 3) .count(); System.out.println("Count: " + count); // Using map for side effects with discarding List<String> processed = names.stream() .map(name -> { System.out.println("Side effect for: " + name); // Side effect return name.toUpperCase(); // Actual transformation }) .collect(Collectors.toList()); // When you only want side effects, use forEach at the end names.stream() .filter(name -> name.startsWith("J")) .forEach(name -> System.out.println("Found J-name: " + name)); // Discarded } // Discarding Optional values public void demonstrateOptionalDiscarding() { Optional<String> optional = Optional.of("value"); // ifPresent - automatically discards the value after consumption optional.ifPresent(value -> System.out.println("Value is present: " + value)); // orElseGet with side effects String result = optional.orElseGet(() -> { System.out.println("Generating default value"); // Side effect return "default"; }); // When you don't care about the value, just whether it's present if (optional.isPresent()) { System.out.println("Optional has a value (but we don't care what it is)"); } } } Real-World Use Cases
Configuration Processing
public class ConfigurationProcessor { public void processConfiguration(Map<String, String> config) { // Discard values for keys we don't care about config.forEach((key, value) -> { switch (key) { case "database.url" -> setupDatabase(value); case "cache.enabled" -> enableCache(Boolean.parseBoolean(value)); case "log.level" -> setLogLevel(value); default -> { // Discard unknown configuration keys System.out.println("Ignoring unknown config: " + key); // 'value' is deliberately not used } } }); } // Pattern matching with configuration objects public void processConfigObject(Object config) { switch (config) { case DatabaseConfig(String url, String username, String _) -> // Discard password for logging purposes System.out.println("Database: " + url + ", user: " + username); case CacheConfig(boolean enabled, int size, String _) -> // Discard cache name System.out.println("Cache enabled: " + enabled + ", size: " + size); case LogConfig(String level, String _, String _) -> // Discard format and file path System.out.println("Log level: " + level); case null, default -> System.out.println("Unknown config type"); } } record DatabaseConfig(String url, String username, String password) {} record CacheConfig(boolean enabled, int size, String cacheName) {} record LogConfig(String level, String format, String filePath) {} private void setupDatabase(String url) { /* implementation */ } private void enableCache(boolean enabled) { /* implementation */ } private void setLogLevel(String level) { /* implementation */ } } Event Processing with Discarding
public class EventProcessor { public void processEvents(List<Event> events) { events.forEach(event -> { switch (event) { case UserLoginEvent(String username, String _, long timestamp) -> // Discard IP address, only care about username and timestamp System.out.println("User logged in: " + username + " at " + timestamp); case UserLogoutEvent(String username, long _) -> // Discard timestamp for logout events System.out.println("User logged out: " + username); case PaymentEvent(String userId, double amount, String _) -> // Discard transaction ID System.out.println("Payment: " + userId + " paid " + amount); case null -> System.out.println("Null event received"); default -> { // Discard unknown event types System.out.println("Unknown event type: " + event.getClass().getSimpleName()); } } }); } // Sealed interface for events public sealed interface Event permits UserLoginEvent, UserLogoutEvent, PaymentEvent { } public record UserLoginEvent(String username, String ipAddress, long timestamp) implements Event {} public record UserLogoutEvent(String username, long timestamp) implements Event {} public record PaymentEvent(String userId, double amount, String transactionId) implements Event {} } API Response Processing
public class ApiResponseHandler { public void handleApiResponse(Response response) { switch (response) { case Success(String data, Map<String, String> _) -> // Discard headers, only process data processSuccessData(data); case Error(int code, String message, Map<String, String> _) -> // Discard error headers handleError(code, message); case Redirect(String url, int _, Map<String, String> _) -> // Discard status code and headers for redirects handleRedirect(url); } } // Processing paginated responses public void processPaginatedData(PageResponse response) { // Discard pagination metadata if we don't need it response.data().forEach(item -> processItem(item) // Only care about the data items ); // Or if we only need pagination info if (response instanceof PageResponse(_, int totalPages, int currentPage, _)) { // Discard data and total items System.out.println("Page " + currentPage + " of " + totalPages); } } record Response() {} record Success(String data, Map<String, String> headers) extends Response {} record Error(int code, String message, Map<String, String> headers) extends Response {} record Redirect(String url, int statusCode, Map<String, String> headers) extends Response {} record PageResponse(List<Object> data, int totalPages, int currentPage, long totalItems) {} private void processSuccessData(String data) { /* implementation */ } private void handleError(int code, String message) { /* implementation */ } private void handleRedirect(String url) { /* implementation */ } private void processItem(Object item) { /* implementation */ } } Best Practices and Patterns
Clear Intent with Discarding
public class DiscardingBestPractices { // 1. Use _ to clearly indicate intentional discarding public void clearIntentExamples() { Map<String, String> config = new HashMap<>(); // Good - clearly shows we're ignoring the value config.forEach((key, _) -> processKey(key)); // Good - clearly shows we're ignoring the key config.forEach((_, value) -> processValue(value)); // Pattern matching with clear discards Object obj = "test"; if (obj instanceof String _) { System.out.println("It's a string (value discarded)"); } } // 2. Avoid confusing discards public void avoidConfusingDiscards() { List<String> items = Arrays.asList("A", "B", "C"); // Potentially confusing - are we really ignoring the parameter? items.forEach(_ -> System.out.println("Processing item")); // Better - use a meaningful name if you actually use it items.forEach(item -> System.out.println("Processing: " + item)); // Or use _ and comment if you must use this pattern items.forEach(_ -> { // We're processing each item but don't need the value performSideEffect(); }); } // 3. Document why you're discarding public void documentedDiscarding() { try { readConfiguration(); } catch (IOException _) { // Discarding exception because we have default configuration useDefaultConfiguration(); } processData((data, metadata) -> { // Using data but discarding metadata as it's not needed for this operation transformData(data); // metadata deliberately discarded with _ }); } // 4. Be careful with overloaded discards public void carefulWithOverloading() { // This can be confusing - which parameter are we discarding? // biConsumer((a, _) -> System.out.println(a)); // biConsumer((_, b) -> System.out.println(b)); // Better to use method references when possible // map.forEach(this::processKeyOnly); // map.forEach(this::processValueOnly); } private void performSideEffect() { /* implementation */ } private void readConfiguration() throws IOException { /* implementation */ } private void useDefaultConfiguration() { /* implementation */ } private void transformData(Object data) { /* implementation */ } private void processKey(String key) { /* implementation */ } private void processValue(String value) { /* implementation */ } } Compatibility and Migration
Handling Older Java Versions
public class BackwardCompatibleDiscarding { // For Java versions before pattern matching public String legacyProcessObject(Object obj) { if (obj instanceof String) { String s = (String) obj; // Have to declare variable return "String: " + s; } else if (obj instanceof Integer) { @SuppressWarnings("unused") Integer i = (Integer) obj; // Have to declare even if unused return "It's an integer"; } else if (obj == null) { return "Null object"; } else { return "Unknown type: " + obj.getClass().getSimpleName(); } } // Using comments to indicate intentional discarding public void legacyDiscardingWithComments() { Map<String, String> map = new HashMap<>(); // Pre-Java 8: have to declare both parameters for (Map.Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); @SuppressWarnings("unused") // Indicate intentional discard String value = entry.getValue(); // Value not used processKeyOnly(key); } } // Migration path for existing code public void migrateToModernDiscarding() { // Old way try { riskyCall(); } catch (Exception e) { // e is declared but not used handleError(); } // New way - clearer intent try { riskyCall(); } catch (Exception _) { // _ clearly shows we're discarding the exception handleError(); } } private void riskyCall() throws Exception { /* implementation */ } private void handleError() { /* implementation */ } private void processKeyOnly(String key) { /* implementation */ } } Conclusion
Variable discarding in Java has evolved significantly, with modern features providing clearer ways to express intent:
- Pattern matching discard (
_) clearly indicates intentional value ignoring - Lambda parameter discarding helps with focused operations
- Exception discarding simplifies error handling when details aren't needed
- Stream API provides built-in mechanisms for value discarding
Key benefits include:
- Clearer code intent -
_explicitly shows value discarding - Reduced warnings - no more "unused variable" warnings for intentional discards
- Better performance in some cases by avoiding unnecessary variable assignments
- Improved readability - code focuses on what's important
Discarding should be used judiciously and documented when the reason isn't immediately obvious from context.