How do I convert byte arrays to hexadecimal and back in Java 17?

When you inspect binary data, a hexadecimal string can be easier to read than a list of decimal byte values. You might use it when troubleshooting a file format or checking a message exchanged between applications.

This tutorial shows a complete conversion in both directions, including readable formatting and invalid input handling.

1. Choose a hexadecimal format

Java 17 introduced java.util.HexFormat as a standard API. No preview or incubator flags are needed.

Use HexFormat.of() for lowercase output without separators. For uppercase output with colons between bytes, use:

HexFormat readable =
        HexFormat.ofDelimiter(":").withUpperCase();

Each byte becomes two hexadecimal digits. These options are documented in the Java 17 HexFormat API.

2. Create the complete example

Save the following as HexFormatExample.java:

import java.util.Arrays;
import java.util.HexFormat;

public class HexFormatExample {
    public static void main(String[] args) {
        byte[] packet = {
                0, 15, 16, 127, (byte) 128, (byte) 255
        };

        HexFormat plain = HexFormat.of();
        HexFormat readable =
                HexFormat.ofDelimiter(":").withUpperCase();

        System.out.println(
                "Compact: " + plain.formatHex(packet));

        String formatted = readable.formatHex(packet);
        System.out.println("Readable: " + formatted);

        byte[] restored = readable.parseHex(formatted);
        System.out.println(
                "Same bytes: " + Arrays.equals(packet, restored));
        System.out.println(
                "Java bytes: " + Arrays.toString(restored));

        try {
            readable.parseHex("00:GG");
        } catch (IllegalArgumentException exception) {
            System.out.println(
                    "Invalid input: expected hexadecimal byte pairs");
        }
    }
}

The program first creates sample data. It then prints two representations, parses the colon-separated version, and checks whether the recovered bytes match the original.

3. Compile and run

Open a terminal in the directory containing the file:

javac --release 17 HexFormatExample.java
java HexFormatExample

Output:

Compact: 000f107f80ff
Readable: 00:0F:10:7F:80:FF
Same bytes: true
Java bytes: [0, 15, 16, 127, -128, -1]
Invalid input: expected hexadecimal byte pairs

4. Understand the result

The two formatted strings represent identical data. Java’s signed byte representation explains the negative decimal values in the recovered array: the bit patterns 80 and FF correspond to -128 and -1.

Arrays.equals() confirms that the conversion preserved the bytes.

Parsing must use the expected delimiters. Invalid digits, such as GG, cause IllegalArgumentException; hexadecimal digit letters themselves are case-insensitive. See HexFormat.parseHex().

Best practices

  • Define the expected representation when exchanging data with another application.
  • Use a matching formatter for encoding and decoding.
  • Keep the original bytes when your next operation requires binary data.
  • Handle invalid external input at the point where it enters your application.
  • Remember that hexadecimal encoding makes data readable; it does not encrypt it.

Summary

Use formatHex() to turn bytes into hexadecimal text and parseHex() to recover them. A short round-trip example helps verify your chosen representation before integrating it into a larger application.

How do I protect a List inside a Java record?

A record is a convenient way to represent data, but a record containing a mutable list can still change unexpectedly. In this tutorial, you will protect a reading list from changes made through other references.

Records became a final language feature in Java 16. This tutorial uses Java 17 as its baseline and requires no preview or incubator flags. See Java’s language changes by release.

1. Understand the problem

Consider this record:

record ReadingList(String title, List<String> books) {
}

Java generates its constructor and accessors. However, the generated constructor stores the supplied list reference. A caller holding that list can change its contents afterward.

A final field prevents reassignment of the reference; it does not freeze the referenced object. Java’s record documentation explains the fields and methods generated for records.

2. Protect the list during construction

Use a compact constructor to replace the incoming list with an unmodifiable snapshot:

ReadingList {
    books = List.copyOf(books);
}

Assign to the constructor parameter, books. Java assigns the resulting value to the component field after the constructor body completes.

List.copyOf() preserves element order and prevents later changes to the original list from affecting the result. It rejects null collections and null elements. See the Java 17 List API.

3. Create the complete example

Save this code as RecordListExample.java:

import java.util.ArrayList;
import java.util.List;

public class RecordListExample {
    record ReadingList(String title, List<String> books) {
        ReadingList {
            if (title == null || title.isBlank()) {
                throw new IllegalArgumentException(
                        "Title must not be blank");
            }
            books = List.copyOf(books);
        }
    }

    public static void main(String[] args) {
        List<String> original =
                new ArrayList<>(List.of("Java Basics"));

        ReadingList readingList =
                new ReadingList("Weekend", original);

        original.add("Advanced Java");

        System.out.println("Original: " + original);
        System.out.println("Record: " + readingList.books());

        try {
            readingList.books().add("Another Book");
        } catch (UnsupportedOperationException exception) {
            System.out.println("Record list cannot be modified");
        }
    }
}

The example checks two paths: modifying the original list and attempting a modification through the record’s accessor.

4. Compile and run

Open a terminal in the directory containing the file:

javac --release 17 RecordListExample.java
java RecordListExample

Output:

Original: [Java Basics, Advanced Java]
Record: [Java Basics]
Record list cannot be modified

The original list accepts another book. The record keeps its initial contents, and its returned list rejects additions.

5. Know the limit: elements are not copied

This approach protects the list structure. It does not create separate copies of its elements.

Here, the elements are strings, which are immutable. If the list contained mutable Book objects, callers could still change those objects. Use immutable element types or copy mutable elements when your data model requires deeper protection.

Best practices

  • Copy incoming collections at construction boundaries.
  • Document your null policy; this example rejects null lists and null elements.
  • Prefer immutable element types for value objects.
  • Avoid unnecessary copying in the accessor when the stored list is already protected.
  • Treat the caught exception as a demonstration; normal application code should avoid attempting unsupported modifications.

Summary

A record does not automatically make a referenced list immutable. Using List.copyOf() in its compact constructor protects the list against external structural changes while keeping the record concise.

How do I use Collectors.partitioningBy?

The Collectors.partitioningBy is a method in Java’s java.util.stream.Collectors class that is used to partition elements of a stream into two groups based on a predicate. It essentially creates a Map with a boolean key (true or false) and lists of elements as values. Here’s an explanation of how to use it effectively:

Syntax:

Collectors.partitioningBy(Predicate<? super T> predicate)

Description:

  1. Predicate: This is a functional interface that tests a condition on elements of the stream. Each element in the stream is evaluated against this condition.
  2. Result: The partitioningBy collector returns a Map with two entries:
    • Key true: Contains elements for which the predicate evaluates to true.
    • Key false: Contains elements for which the predicate evaluates to false.

Example:

Here’s an example usage of partitioningBy:

package org.kodejava.util.stream;

import java.util.*;
import java.util.stream.Collectors;

public class PartitioningExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Partition numbers into even and odd
        Map<Boolean, List<Integer>> partitions = numbers.stream()
                .collect(Collectors.partitioningBy(num -> num % 2 == 0));

        // Access partitions
        List<Integer> evens = partitions.get(true);  // Numbers divisible by 2 (even numbers)
        List<Integer> odds = partitions.get(false); // Numbers not divisible by 2 (odd numbers)

        System.out.println("Even Numbers: " + evens);
        System.out.println("Odd Numbers: " + odds);
    }
}

Output:

Even Numbers: [2, 4, 6, 8, 10]
Odd Numbers: [1, 3, 5, 7, 9]

Advanced Usage:

You can extend the functionality of partitioningBy by combining it with other collectors, such as Collectors.mapping or Collectors.counting.

Example: Count of elements in each partition

Map<Boolean, Long> partitionedCount = numbers.stream()
        .collect(Collectors.partitioningBy(num -> num % 2 == 0, Collectors.counting()));

System.out.println(partitionedCount);
// Output: {false=5, true=5}

In this example, instead of partitioning into lists, the partitioning is configured to count the number of elements in each group.


When to Use partitioningBy:

Use Collectors.partitioningBy when:

  • You need to classify a collection of items into two mutually exclusive groups.
  • The condition for classification is a boolean predicate.

It’s commonly used in scenarios like:

  • Splitting numbers into even and odd.
  • Categorizing people into adults and minors based on age.
  • Determining whether elements in a list satisfy a specific condition, e.g., “passing grade” or “failing grade.”

How do I use Map.merge() to simplify counting logic?

The Map.merge method in Java is a convenient way to simplify various kinds of logic that require updating or modifying values in a map, such as counting occurrences. It works by letting you specify how to combine the old value (if it exists) and the new value (to be added). This is particularly useful for implementing counting logic more concisely.

Here’s how you can use Map.merge to count occurrences:

Key Idea

  • If the key doesn’t exist in the map, merge inserts it with the given value.
  • If the key already exists, merge uses the provided function (a BiFunction) to combine the existing value and the new value.

Example: Counting Word Occurrences in a String

package org.kodejava.util;

import java.util.HashMap;
import java.util.Map;

public class WordCounter {
    public static void main(String[] args) {
        String text = "apple banana apple orange banana apple";

        // Split the string into words
        String[] words = text.split(" ");

        // Map to store word counts
        Map<String, Integer> wordCounts = new HashMap<>();

        // Use Map.merge to simplify counting logic
        for (String word : words) {
            // Increment count for each word
            wordCounts.merge(word, 1, Integer::sum);
        }

        // Print the word counts
        System.out.println(wordCounts);
    }
}

Explanation of merge Usage

In the above example:

  1. wordCounts.merge(word, 1, Integer::sum);
    • word is the key.
    • 1 is the value to add (for each occurrence of the word).
    • Integer::sum is the combining function that adds the existing value (if present) and the new value.
      • If the word is already in the map, the count is increased by 1.
      • If the word is not in the map, it is added with an initial count of 1.

Advantages of Using Map.merge for Counting

  • Conciseness: Avoids the need for verbose if-else or containsKey checks.
  • Thread Safety: Works well in a thread-safe map (e.g., ConcurrentHashMap) without requiring additional synchronization.
  • Readability: The code is clear and easy to understand, as the counting logic is encapsulated in a single line.

Without Map.merge

To see why Map.merge simplifies the code, here’s how the same logic would look without it:

for (String word : words) {
    if (wordCounts.containsKey(word)) {
        wordCounts.put(word, wordCounts.get(word) + 1);
    } else {
        wordCounts.put(word, 1);
    }
}

As you can see, it’s more verbose and repetitive compared to using merge.


Other Use Cases for Map.merge

  1. Updating a map with custom logic:
    You can combine values in a way that suits your requirements, such as concatenating strings or appending to a list.

  2. Tracking multiple values:
    For example, storing a list of values associated with a key while avoiding null checks:

    map.merge(key, new ArrayList<>(List.of(value)), (oldList, newList) -> {
       oldList.addAll(newList);
       return oldList;
    });
    
  3. Combining maps:
    Merge entries from one map into another map using custom logic.


In summary, Map.merge helps to simplify and streamline your counting or updating logic by focusing on what to do with existing and new values, while handling key-insertion logic for you.

How to use the new API enhancements in java.nio.file in Java 17

Java 17 introduced several significant enhancements in the java.nio.file package, focusing on improving file system operations, security, and performance. Below is an explanation of the new APIs and available enhancements, with examples demonstrating how to use them.

Key API Enhancements in java.nio.file for Java 17

1. Files.mismatch()

The method Files.mismatch(Path, Path) was added to efficiently compare two files. It helps identify the position where two files differ or returns -1 if the files are identical.

Example:

package org.kodejava.nio;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FilesMismatchExample {
    public static void main(String[] args) throws IOException {
        Path file1 = Path.of("file1.txt");
        Path file2 = Path.of("file2.txt");

        // Create sample files
        Files.writeString(file1, "Hello, world!");
        Files.writeString(file2, "Hello, Java!");

        long mismatchPosition = Files.mismatch(file1, file2);

        if (mismatchPosition == -1) {
            System.out.println("Files are identical.");
        } else {
            System.out.println("Files differ beginning at byte position: " + mismatchPosition);
        }
    }
}

Usage Notes:

  • This method is especially useful for large files where reading and comparing the entire contents manually would be inefficient.
  • For identical files, the method returns -1.

2. Files.copy() Enhancements

The Files.copy(InputStream in, Path target, CopyOption... options) method now supports the StandardCopyOption.REPLACE_EXISTING option to overwrite existing files directly.

Example:

package org.kodejava.nio;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class FilesCopyExample {
    public static void main(String[] args) throws Exception {
        Path targetPath = Path.of("output.txt");

        try (InputStream inputStream = new ByteArrayInputStream("File content".getBytes())) {
            Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
        }
        System.out.println("File copied successfully to: " + targetPath);
    }
}

Usage Notes:

  • Prior to Java 17, replacing existing files required explicitly deleting the file first.
  • This enhancement simplifies file replacement logic.

3. Support for Hidden Files in Files.isHidden()

Java 17 improves the handling of hidden files for certain platforms where determining this attribute was inconsistent (e.g., macOS and Linux).

Example:

package org.kodejava.nio;

import java.nio.file.Files;
import java.nio.file.Path;

public class HiddenFileExample {
    public static void main(String[] args) throws Exception {
        Path filePath = Path.of(".hiddenFile");
        Files.createFile(filePath);

        if (Files.isHidden(filePath)) {
            System.out.println(filePath + " is a hidden file.");
        } else {
            System.out.println(filePath + " is not a hidden file.");
        }
    }
}

4. File Permission Enhancements on Unix-like Systems

Java 17 improves security and performance for managing file permissions using PosixFilePermissions.

Example:

package org.kodejava.nio;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;

public class FilePermissionExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("example.txt");
        Files.createFile(path);

        Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-r--r--");
        Files.setPosixFilePermissions(path, permissions);

        System.out.println("File permissions: " + Files.getPosixFilePermissions(path));
    }
}

Usage Note:

  • This improvement provides more robust support for file permissions on Unix-like operating systems.

Summary Table of Changes

Enhancement Description Java Version
Files.mismatch() Compares two files to find the first mismatch position or confirms equality Java 17
Enhanced Files.copy() Overwrite files without manually deleting them Java 17
Improved Files.isHidden() Better cross-platform handling of hidden files Java 17
File Permission Enhancements Improved security and performance on Unix-like systems Java 17

These enhancements improve efficiency, accessibility, and usability when working with file system operations. You can start using them to simplify your file-handling logic in Java applications.