java
57 lines · 9 steps
Parsing and building URL query strings in Java
An immutable-ish wrapper that parses a URI's query into a multimap and rebuilds it, handling repeated keys and encoding.
Explained by
highlit
1public final class QueryParams {
2
3 private final Map<String, List<String>> params;
4
5 private QueryParams(Map<String, List<String>> params) {
6 this.params = params;
7 }
8
9 public static QueryParams parse(URI uri) {
10 String query = uri.getRawQuery();
11 Map<String, List<String>> parsed = new LinkedHashMap<>();
12 if (query == null || query.isEmpty()) {
13 return new QueryParams(parsed);
14 }
15 for (String pair : query.split("&")) {
16 int eq = pair.indexOf('=');
17 String key = eq >= 0 ? pair.substring(0, eq) : pair;
18 String value = eq >= 0 ? pair.substring(eq + 1) : "";
19 parsed.computeIfAbsent(decode(key), k -> new ArrayList<>()).add(decode(value));
20 }
21 return new QueryParams(parsed);
22 }
23
24 public Optional<String> first(String key) {
25 List<String> values = params.get(key);
26 return values == null || values.isEmpty() ? Optional.empty() : Optional.of(values.get(0));
27 }
28
29 public List<String> all(String key) {
30 return params.getOrDefault(key, List.of());
31 }
32
33 public QueryParams with(String key, String value) {
34 params.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
35 return this;
36 }
37
38 public QueryParams without(String key) {
39 params.remove(key);
40 return this;
41 }
42
43 public String toQueryString() {
44 StringJoiner joiner = new StringJoiner("&");
45 params.forEach((key, values) -> values.forEach(v ->
46 joiner.add(encode(key) + "=" + encode(v))));
47 return joiner.toString();
48 }
49
50 private static String decode(String s) {
51 return URLDecoder.decode(s, StandardCharsets.UTF_8);
52 }
53
54 private static String encode(String s) {
55 return URLEncoder.encode(s, StandardCharsets.UTF_8);
56 }
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Query keys can repeat, so a Map of List models them correctly where a plain Map would silently drop values.
- 2computeIfAbsent lets you lazily create the inner collection only when a key first appears.
- 3Always decode on parse and encode on output so special characters survive a round trip.
Related explainers
java
@RestController @RequestMapping("/api/files") public class FileUploadController {
Handling secure file uploads in Spring
file-upload
input-validation
path-traversal
Intermediate
10 steps
java
package com.example.fs; import java.io.IOException; import java.nio.file.FileSystems;
Watching a directory with Java NIO
file-io
event-loop
resource-management
Intermediate
8 steps
rust
use std::collections::HashMap; use std::fmt; #[derive(Debug)]
Parsing a config file with typed errors in Rust
error-handling
enums
parsing
Intermediate
9 steps
java
public final class ShippingAddress { private final String recipient; private final String line1; private final String line2;
Building immutable objects with the Builder pattern
builder-pattern
immutability
validation
Intermediate
7 steps
java
import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter;
Converting timestamps across time zones in Java
date-time
time-zones
instant
Intermediate
6 steps
java
package com.example.payments.config; import com.stripe.StripeClient; import org.springframework.beans.factory.annotation.Value;
Swapping payment gateways by profile in Spring
dependency-injection
profiles
configuration
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/parsing-and-building-url-query-strings-in-java-explained-java-bc66/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.