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.