Java version: Java 17 (LTS) Style: Playbook / Cookbook — independent, practical recipes
Table of Contents
- 2.1 Introduction and philosophy
- 2.2 Prerequisites
- 2.3 Execute Java code
- 2.4 Exercise files
- 2.5 Modules overview
- 3.1 Introduction to Strings
- 3.2 Remove spaces (
trimvsstrip) - 3.3 Empty string or blank? (
isEmptyvsisBlank) - 3.4 Transforming a String — the new approach (
transform) - 3.5 Compare Strings correctly
- 3.6 Iterate over characters
- 3.7 Check the contents of a String
- 3.8 Find and replace matches
- 3.9 Iterate over multiple lines
- 3.10 Tokenize a String (
split) - 3.11 Join Strings
- 3.12 Building Strings in a Loop (
StringBuilder) - 3.13 Prefer Text Blocks
- 3.14 Text localization (
ResourceBundle) - 3.15 Generate random Strings (
UUID) - 3.16 Use Apache Commons Lang
- 3.17 Demo application: data extraction and cleaning
- 4.1 Introduction — primitive types and wrapper classes
- 4.2 Convert a String to a number
- 4.3 Check if a String is a number
- 4.4 Format numbers (
DecimalFormat) - 4.5 Localized and compact formatting (
NumberFormat,Locale) - 4.6 Compare integers (
Integerwrapper and JVM cache) - 4.7 Floating-point arithmetic (
BigDecimal) - 4.8 Very large numbers (
BigInteger,BigDecimal) - 4.9 Rounding with the
Mathclass - 4.10 Rounding with
DecimalFormatandBigDecimal - 4.11 Generate random values (
ThreadLocalRandom) - 4.12 Demo application 1: convert a list of formatted numbers
- 4.13 Demo application 2: rounding and calculating VAT
- 5.1 Introduction
- 5.2 Date-Time API Overview
- 5.3 Calculate the difference between two dates (
Period) - 5.4 Compare two dates (
isBefore,isAfter) - 5.5 First or last day of a period (
TemporalAdjusters) - 5.6 Manage recurring events
- 5.7 Add or subtract dates
- 5.8 Demo application 1: time remaining in the year
- 5.9 Format dates (
DateTimeFormatter) - 5.10 Handle multiple date formats
- 5.11 Convert from/to old
Date - 5.12 Get current time (
Instant,LocalDateTime,ZoneId) - 5.13 List all
ZoneId - 5.14 Start and end of day (
LocalTime.MAX) - 5.15 Calculate arrival time in another time zone
- 5.16 Demo application 2: current time in other zones
- 6.1 Introduction
- 6.2 Add an element to an array (
Arrays.copyOf) - 6.3 Convert between array and List
- 6.4 Create a List and add elements
- 6.5 Checking if a List is empty (
isEmpty) - 6.6 Remove elements from a List (
removeIf) - 6.7 Remove duplicates (
HashSet) - 6.8 Find common and different elements in two Lists
- 6.9 Replacing elements in a List (
replaceAll) - 6.10 Simple sorting of a List (
Comparator) - 6.11 Advanced sorting with custom types
- 6.12 Map Overview
- 6.13 Find duplicate values in a Map
- 6.14 Delete and replace elements in a Map
- 6.15 Check equality of two Map (
equals,hashCode) - 6.16 Sort a Map (
TreeMap,Stream) - 6.17 Merge Map (
putAll,flatMap,toMap) - 6.18 Third-party libraries: Apache Collections and Google Guava
- 7.1 Introduction
- 7.2 IO vs NIO vs NIO2
- 7.3 Conversion between
FileandPath - 7.4 Use the correct path separator
- 7.5 Determine home directory
- 7.6 Check access to a file (
isRegularFile,isReadable) - 7.7 Read small text files (
Files.readString) - 7.8 Read large text files (
BufferedReader,Files.lines) - 7.9 Read and write binary files
- 7.10 Write to file (
Files.writeString,StandardOpenOption) - 7.11 Copy a file (
Files.copy) - 7.12 Move or rename a file (
Files.move) - 7.13 Delete a file (
Files.delete) - 7.14 Iterate over files in a directory (
DirectoryStream) - 7.15 Walk file tree (
walkFileTree,FileVisitor) - 7.16 Create directory (
createDirectory,createDirectories) - 7.17 Copy and delete a directory (Apache
FileUtils) - 7.18 Demo application: collect and copy log files with errors
- 7.19 To go further
1. Course Overview
Module 1 — 1m 12s
This course, Java Playbook, is a collection of practical recipes for common Java tasks. The basic idea is that a program, no matter how complex, can be broken down into a sequence of smaller, simpler commands. This course teaches how to code a wide range of common tasks that can be put together to create real software.
Main themes covered:
- Transform strings and numbers
- Manage dates and times
- Working with collections
- Write file operations concisely
Objective: At the end of the course, you will be able to quickly write code that implements common business functionalities.
Prerequisites: Professional experience with Java in any IDE (IntelliJ recommended).
2. Introduction to Java Recipes
Module 2 — 6m 32s
2.1 Introduction and philosophy
This course is Playbook (or Cookbook) style: a collection of independent recipes for many common challenges in Java — strings, numbers, collections, reading files, etc.
Important points:
- The course was created with Java 17 (the latest LTS version available at the time of recording), but is 100% applicable to recent versions of Java.
- The majority of recipes also apply to older versions (Java 8, 11).
- Features specific to new versions are clearly marked.
- Modules can be viewed in any order (minimal cross-references).
- Very practical course: lots of code demonstrations, few slides.
2.2 Prerequisites
- Java developers or anyone who needs to write occasional Java.
- Suitable for beginners who already have the basics of the language (mastered syntax).
- Recommended: at least 6 months of experience in Java.
Minimum required:
- Java 17 or higher installed.
- Able to write and run a Hello World program in Java.
- Ideally, knowledge of a build tool like Maven (only necessary for a few recipes).
The author uses IntelliJ Community Edition on Windows, but Eclipse, VS Code, macOS or Linux work just as well.
2.3 Execute Java code
Two methods to execute the code:
Method 1 — JShell (REPL):
java -version # vérifier la version (>= 17)
jshell # lancer le REPL
In JShell, you can declare variables and evaluate them without the need for .java files. Semicolons are optional. Practical for small tests, but not ideal for recipes of 10-20 lines (no autocompletion).
Method 2 — IDE (IntelliJ recommended):
- Each recipe is written in a new class with a
mainmethod. - For IntelliJ: check in
Settings > Java Compilerthat the target Java version is the most recent, then underProject Structureat theProjectandModulelevel. - Multi-module Maven project organization: one module per course module (
m2strings,m3numbers, etc.).
2.4 Exercise files
Two options to access the code:
- Public GitHub from author (Andrejs Doronins): repo Java17-Playbook.
- Pluralsight course page: Exercise files tab → download the zip file.
2.5 Module overview
| Module | Subject |
|---|---|
| Module 3 | Recipes for thongs |
| Module 4 | Recipes for numbers |
| Module 5 | Dates and times |
| Module 6 | Paintings and collections |
| Module 7 | I/O code (files) |
3. Transform Strings
Module 3 — 48m 33s
3.1 Introduction to Strings
Strings are an inevitable part of almost any programming task. Reminder: in Java, String is not a primitive type — it is a class with methods. For each primitive type, there is a corresponding wrapper class (int ↔ Integer, double ↔ Double, etc.), but there is no String primitive type.
This module does not cover trivial single-invocation methods (substring, toUpperCase, startsWith, etc.) — except when there is a catch or subtlety.
Module progression: from simplest to most complex: transformation of a single string → comparison → iteration → tokenization → reconstruction.
3.2 Remove spaces (trim vs strip)
Problem: Remove leading and trailing spaces from a string.
trim() (Java 1):
- Removes characters whose Unicode code is ≤ U+0020 (standard space).
- Covers: space, tab, carriage return, line feed.
- Does not support Unicode spaces added after U+0020.
strip() (Java 11):
- Unicode-aware version of
trim(). - Removes all characters considered whitespace by the Unicode specification.
- Recommended in modern code.
Related methods:
stripLeading(): removes leading spaces only.stripTrailing(): only removes trailing spaces.
// Fichier : m2strings/src/main/java/strings/RemoveSpacesDemo.java
package strings;
public class RemoveSpacesDemo {
public static void main(String[] args) {
System.out.println("hello ".trim() + " there".trim());
char space = '\u2002';
System.out.println("\\u2002 is whitespace: " + Character.isWhitespace(space));
System.out.println("hello\u2002".trim() + "\u2002there".trim()); // trim ne fonctionne pas
System.out.println("hello\u2002".strip() + "\u2002there".strip()); // strip fonctionne
}
}
Explanation: The character \u2002 (IN SPACE) is a valid Unicode space recognized by Character.isWhitespace() and handled by strip() but not by trim().
3.3 Empty string or blank? (isEmpty vs isBlank)
Basic difference:
isEmpty(): returnstrueonly if the length of the string is zero (length() == 0).isBlank()(Java 11): returnstrueif the string is empty or contains only space characters (Unicode whitespace).
Pitfalls to avoid:
- A string containing only spaces (
" ","\r","\u2002") is not empty (isEmpty()→false) but it is blank (isBlank()→true). - Writing
str.strip().isBlank()is redundant — usingisBlank()directly. - Avoid
str.trim().isEmpty()which can produce unexpected results with exotic Unicode spaces.
// Fichier : m2strings/src/main/java/strings/EmptyBlankDemo.java
package strings;
public class EmptyBlankDemo {
public static void main(String[] args) {
System.out.println("isEmpty()");
System.out.println("".isEmpty()); // True
System.out.println("\r".isEmpty()); // False
System.out.println("\u2002".isEmpty()); // False
System.out.println(" ".isEmpty()); // False
System.out.println("isBlank()");
System.out.println("".isBlank()); // True
System.out.println("\r".isBlank()); // True
System.out.println("\u2002".isBlank()); // True
System.out.println(" ".isBlank()); // True
System.out.println("Careful!");
String evilString = "\u2002";
System.out.println(evilString.trim().isEmpty()); // False! résultat inattendu
System.out.println(evilString.strip().isBlank()); // redundant mais correct
System.out.println(evilString.isBlank()); // true — préférer ceci
}
}
3.4 Transforming a String — the new approach (transform)
Problem: Chaining multiple transformations on a string, including custom functions with conditional logic, without breaking the method chain.
Old approach: Save intermediate results in local variables, then pass them to methods.
New approach — transform() (Java 12):
- Takes a
Function<String, R>as a parameter. - Allows you to insert any logic (lambda or reference method) into a method chain.
- Makes code more readable by maintaining continuous flow.
// Fichier : m2strings/src/main/java/strings/TransformStringDemo.java
package strings;
public class TransformStringDemo {
public static void main(String[] args) {
String lotteryWin = " 100 usd ";
// Approche classique avec variable intermédiaire
String result = lotteryWin
.replaceAll("[a-z]", "")
.strip();
String formattedResult = formatNumber(result);
System.out.println(formattedResult.toUpperCase());
// Avec transform() — chaîne ininterrompue
String finalResult = lotteryWin
.replaceAll("[a-z]", "")
.strip()
.transform(TransformStringDemo::formatNumber)
.toUpperCase();
System.out.println(finalResult);
// Version utilisant uniquement transform()
String finalResult2 = lotteryWin
.transform(s -> s.replaceAll("[a-z]", ""))
.transform(String::strip)
.transform(TransformStringDemo::formatNumber)
.transform(String::toUpperCase);
System.out.println(finalResult2);
}
private static String formatNumber(String num) {
if (Integer.parseInt(num) < 100) {
return "Nice! You've won: " + num;
} else {
return "Great news! You've won: " + num;
}
}
}
3.5 Compare Strings correctly
Fundamental rule: Never never use == to compare strings in Java — this compares references in memory, not contents.
Context — String Pool:
- The JVM optimizes memory by storing string literals in a pool. Two identical strings from source code point to the same object →
==may accidentally returntrue. - A
new String("abc")creates a separate object out of the pool →==will returnfalseeven for identical contents.
Methods to use:
equals(): exact comparison, case sensitive.equalsIgnoreCase(): case-insensitive comparison.
Best defensive practice: Place the known non-null string first to avoid a NullPointerException:
// RISQUE : si input est null → NullPointerException
input.equals("expected");
// SÛRE : renvoie false si input est null
"expected".equals(input);
// Fichier : m2strings/src/main/java/strings/CompareStringsDemo.java
package strings;
public class CompareStringsDemo {
public static void main(String[] args) {
System.out.println("abc" == "abc"); // true (String pool)
String s1 = "abc";
String s2 = new String("abc"); // objet séparé hors du pool
System.out.println(s1 == s2); // false — références différentes
System.out.println(s1.equals(s2)); // true — contenu identique
}
}
3.6 Iterate over characters
Problem: Java does not allow you to iterate directly over a string with a for-each (a String is not an array of char).
Method 1 — classic for loop with charAt():
for (int i = 0, n = str.length(); i < n; i++) {
char c = str.charAt(i);
}
Method 2 — convert to char array with toCharArray():
for (char c : str.toCharArray()) {
// traiter c
}
Best practice: Use a StringBuilder to construct the resulting string in a loop (memory optimization).
Note: To compare char (primitive type), use == (and not equals()).
// Fichier : m2strings/src/main/java/strings/IterateOverCharactersDemo.java
package strings;
public class IterateOverCharactersDemo {
public static void main(String[] args) {
String str = "some string";
for (int i = 0, n = str.length(); i < n; i++) {
char c = str.charAt(i);
}
for (char c : str.toCharArray()) {
System.out.println(c);
}
System.out.println("Specific char to uppercase: ");
System.out.println(charToUpperCase(str, 's'));
System.out.println(charToUpperCase(str, 'g'));
}
// Transforme un caractère spécifique en majuscule
private static String charToUpperCase(String str, char charToUpper) {
var sb = new StringBuilder();
for (char c : str.toCharArray()) {
char charToAppend = c == charToUpper ? Character.toUpperCase(c) : c;
sb.append(charToAppend);
}
return sb.toString();
}
}
3.7 Check the contents of a String
Problem: Checking if a string contains only numbers, letters, or a specific set of characters.
Three approaches:
1 — Loop + Character.isDigit():
public static boolean containsOnlyDigitsLoop(String str) {
for (char c : str.toCharArray()) {
if (!Character.isDigit(c)) return false;
}
return true;
}
Warning: Character.isDigit() recognizes Arabic-Indicated and Devanagari (Unicode) numerals, not just 0-9.
2 — Streams + allMatch() (Java 8+):
public static boolean containsOnlyDigitsFunctional(String str) {
return str.chars().allMatch(Character::isDigit);
}
3 — Regex with matches():
public static boolean containsOnlyDigitsRegex(String str) {
return str.matches("[0-9]+");
}
Generic version with IntPredicate:
// Fichier : m2strings/src/main/java/strings/ContainsOnlySpecificCharDemo.java
package strings;
import java.util.function.IntPredicate;
public class ContainsOnlySpecificCharDemo {
public static void main(String[] args) {
IntPredicate isDigit = Character::isDigit;
IntPredicate isLetter = Character::isLetter;
IntPredicate isLetterOrDigit = Character::isLetterOrDigit;
System.out.println(containsOnlyCharacter("123", isDigit)); // true
}
public static boolean containsOnlyCharacter(String str, IntPredicate predicate) {
return str.chars().allMatch(predicate);
}
}
// Fichier : m2strings/src/main/java/strings/ContainOnlyDigitDemo.java
package strings;
public class ContainOnlyDigitDemo {
private static final String ONLY_DIGITS = "123456789";
private static final String NOT_ONLY_DIGITS = "123456789A";
public static void main(String[] args) {
System.out.println("Character.isDigit() solution:");
System.out.println("Contains only digits: " + containsOnlyDigitsLoop(ONLY_DIGITS)); // true
System.out.println("Contains only digits: " + containsOnlyDigitsLoop(NOT_ONLY_DIGITS)); // false
System.out.println("Java 8, functional-style solution:");
System.out.println("Contains only digits: " + containsOnlyDigitsFunctional(ONLY_DIGITS));
System.out.println("Contains only digits: " + containsOnlyDigitsFunctional(NOT_ONLY_DIGITS));
System.out.println("String.matches() solution:");
System.out.println("Contains only digits: " + containsOnlyDigitsRegex(ONLY_DIGITS));
System.out.println("Contains only digits: " + containsOnlyDigitsRegex(NOT_ONLY_DIGITS));
}
public static boolean containsOnlyDigitsLoop(String str) {
for (char c : str.toCharArray()) {
if (!Character.isDigit(c)) return false;
}
return true;
}
public static boolean containsOnlyDigitsFunctional(String str) {
return str.chars().allMatch(Character::isDigit);
}
public static boolean containsOnlyDigitsRegex(String str) {
return str.matches("[0-9]+");
}
}
3.8 Find and replace matches
The matches() method:
- Performs an exact match (the entire string must match).
- Accepts a regex.
Warning: str.matches("Java 17") returns false if str = "Java 17 Recipes!" — this is not a partial match.
With regex:
str.matches("[Jj]ava.*") // true
str.matches("Java [0-9]+ Recipes!") // true
Replacement:
replace(CharSequence target, CharSequence replacement): replaces all occurrences of a sequence of characters (no regex).replaceAll(String regex, String replacement): replaces all occurrences according to a regex.replaceFirst(String regex, String replacement): replaces only the first occurrence.
// Fichier : m2strings/src/main/java/strings/FindAndReplaceDemo.java
package strings;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FindAndReplaceDemo {
public static void main(String[] args) {
findMatches();
replace();
}
public static void findMatches() {
String str = "Java 17 Recipes!";
System.out.println(str.matches("Java 17 Recipes!")); // exact match - true
System.out.println(str.matches("Java 17")); // not exact match - false
System.out.println(str.matches("[Jj]ava.*")); // true
System.out.println(str.matches("Java [0-9]+ Recipes!")); // true
}
private static void replace() {
String str = "11 Recipes for Java11";
System.out.println(str.replace("11", "17")); // remplace tout (CharSequence)
System.out.println(str.replaceAll("11", "17")); // remplace tout (regex)
System.out.println(str.replaceFirst("11", "17")); // remplace seulement la première occurrence
}
}
3.9 Iterate over multiple lines
Recommended method — lines() (Java 11):
- Recognizes line terminators:
\n,\r,\r\n. - Returns a
Stream<String>. - Prefer
forEachOrdered()orforEach()with a lambda.
Example with line numbering (AtomicInteger):
// Fichier : m2strings/src/main/java/strings/StreamLinesDemo.java
package strings;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
public class StreamLinesDemo {
public static void main(String[] args) {
String str = "To whom it may concern \n" +
"I wish you a good day \n" +
"Sincerely \n" +
"Me";
Stream<String> lines = str.lines();
// Ajouter des numéros de ligne
final AtomicInteger i = new AtomicInteger(1);
lines.forEach(line -> System.out.println(i.getAndIncrement() + " " + line));
}
}
Important: A Stream can only be used once. Consuming the stream a second time throws an IllegalStateException at runtime.
3.10 Tokenize a String (split)
split(String regex): Splits a string according to a delimiter (regex). Returns an array of strings (String[]).
Comparison lines() vs split("\n"):
lines()automatically handles all line terminators (\n,\r,\r\n) — preferred.split("\n")only handles\n— use if simplified behavior is sufficient.
// Fichier : m2strings/src/main/java/strings/TokenizeDemo.java
package strings;
public class TokenizeDemo {
public static void main(String[] args) {
String str = "To whom it may concern \n" +
"I wish you a good day \n" +
"Sincerely \n" +
"Me";
// Tokeniser par lignes + StringBuilder
String[] lines = str.split("\n");
var sb = new StringBuilder();
for (String line : lines) {
sb.append("-> ").append(line);
}
System.out.println(sb);
// Tokeniser par virgule, obtenir tous les 2 éléments
String text = "Tokyo, 37000000, New York, 20000000, Paris, 11000000";
String[] lines2 = text.split(",");
for (int i = 0; i < lines2.length; i = i + 2) {
System.out.println(lines2[i]);
}
// Combinaison : lignes puis tokens dans chaque ligne
for (String line : str.split("\n")) {
for (String token : line.split(",")) {
// traitement
}
}
}
}
3.11 Joining Strings
Multiple ways to join strings:
1 — Old method (history) — StringBuilder + loop: Verbose method, to be avoided.
2 — String.join() (Java 8): Simplest solution for a fixed delimiter.
String.join(";", "a", "b", "c") // "a;b;c"
3 — StringJoiner (Java 8): Useful when you want a prefix and/or a suffix.
StringJoiner joiner = new StringJoiner(";", "[", "]");
joiner.add("a"); joiner.add("b");
joiner.toString(); // "[a;b]"
4 — Streams + Collectors.joining(): Allows you to filter and transform before joining.
Stream.of(args)
.filter(Objects::nonNull)
.filter(s -> !s.isEmpty())
.collect(Collectors.joining(delimiter));
// Fichier : m2strings/src/main/java/strings/JoinStringsDemo.java
package strings;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class JoinStringsDemo {
public static void main(String[] args) {
String delimiter = ";";
String[] strings = {"11", "12", null, "13", "14", "15", "16", "17"};
System.out.println(joinOldWay(delimiter, strings));
System.out.println(joinSimplest(delimiter, strings));
System.out.println(joinWithJoiner(delimiter, strings));
System.out.println(joinWithStream(delimiter, strings));
}
public static String joinOldWay(String delimiter, String... args) {
StringBuilder result = new StringBuilder();
int i;
for (i = 0; i < args.length - 1; i++) {
result.append(args[i]).append(delimiter);
}
result.append(args[i]);
return result.toString();
}
public static String joinSimplest(String delimiter, String... args) {
return "[" + String.join(delimiter, args) + "]";
}
public static String joinWithJoiner(String delimiter, String... args) {
var joiner = new StringJoiner(delimiter, "{", "}");
for (String arg : args) {
joiner.add(arg);
}
return joiner.toString();
}
public static String joinWithStream(String delimiter, String... args) {
return Stream.of(args)
.filter(Objects::nonNull)
.filter(s -> !s.isEmpty())
.collect(Collectors.joining(delimiter));
}
}
3.12 Building Strings in a Loop (StringBuilder)
Problem: Strings are immutable in Java. Any operation on a string creates a new object in memory. In a loop with a million iterations, this generates a million useless objects.
Solution: StringBuilder — uses append() and modifies the object in place, optimizing memory.
Useful StringBuilder API:
append(): concatenates.delete(): deletes characters.insert(): inserts characters.replace(): replaces characters.reverse(): reverse the string in one line!
For multi-threaded applications: Use StringBuffer (thread-safe, API identical to StringBuilder).
// Fichier : m2strings/src/main/java/strings/BuildStringsInLoop.java
package strings;
public class BuildStringsInLoop {
public static void main(String[] args) {
var builder = new StringBuffer("abc").reverse(); // thread-safe
System.out.println(builder); // "cba"
}
}
3.13 Prefer Text Blocks
Problem: Writing multi-line strings (HTML, JSON, SQL) with concatenations and \n is difficult to read and maintain.
Solution: Text Blocks (Java 15+):
- Delimited by
"""on both sides. - Preserve natural text formatting.
- Eliminate escape characters and concatenations.
// Fichier : m2strings/src/main/java/strings/MultilineStringsDemo.java
package strings;
public class MultilineStringsDemo {
public static void main(String[] args) {
// Text Block simple
String str = """
To whom it may concern
I wish you a good day
Sincerely
Me""";
// Text Block HTML
String textBlock =
"""
<html>
<body>
<tag>
</tag>
</body>
</html>""";
}
}
3.14 Text localization (ResourceBundle)
Problem: A multilingual application should not have conditional code per language (poorly scalable, requires recompilation to correct translations).
Solution: Java localization API (ResourceBundle):
Steps:
- Create
.propertiesfiles for each language (eg:shop_en_US.properties,shop_fr_FR.properties) with key-value pairs. - Create corresponding
Localeobjects. - Use
ResourceBundle.getBundle(baseName, locale)to load the correct file. - Get the values via
bundle.getString(key).
// Fichier : m2strings/src/main/java/strings/LocalizationDemo.java
package strings;
import java.util.Locale;
import java.util.ResourceBundle;
public class LocalizationDemo {
public static void main(String[] args) {
Locale en_US = new Locale("en", "US");
Locale fr_FR = new Locale("fr", "FR");
Locale es_ES = new Locale("es", "ES");
printMessage(en_US);
printMessage(fr_FR);
printMessage(es_ES);
}
private static void printMessage(Locale locale) {
ResourceBundle bundle = ResourceBundle.getBundle("shop", locale);
System.out.println(bundle.getString("greeting"));
}
}
To go further: Pluralsight Implementing Localization in Java course.
3.15 Generate random Strings (UUID)
Quick fix — UUID.randomUUID():
String uuid = UUID.randomUUID().toString();
// ex : "550e8400-e29b-41d4-a716-446655440000"
// Pour supprimer les tirets et tronquer :
uuid.replace("-", "").substring(0, 10);
Limitations: Fixed format with only Latin letters and numbers.
Practical issues: Security, variable length, specific character set, performance, thread-safety → use an appropriate library (eg: Spring Security for Spring apps).
// Fichier : m2strings/src/main/java/strings/RandomString.java
package strings;
import java.util.UUID;
public class RandomString {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
String uuid = UUID.randomUUID().toString();
System.out.println(uuid.replace("-", "").substring(0, 10));
}
}
}
3.16 Using Apache Commons Lang
Maven dependency:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
Class StringUtils (useful methods):
isAllLowercase(),isAllUppercase()abbreviate(text, maxLength): truncates to max length and adds...capitalize(): capitalizes the first lettercontainsAny(),containsNone()(overloaded)stripAccents(): removes accents from Latin letters
Class RandomStringUtils:
randomAlphabetic(min, max): generates a random alphabetic string of variable length
Warning: Do not include dependencies lightly. Every dependency has a maintenance cost and security risks. If you only need 10 lines of code from an open source library, sometimes it’s better to rewrite them.
// Fichier : m2strings/src/main/java/strings/ApacheDemo.java
package strings;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
public class ApacheDemo {
public static void main(String[] args) {
System.out.println(StringUtils.stripAccents("éclair")); // "eclair"
System.out.println(StringUtils.abbreviate("This is a long text", 10)); // "This is..."
System.out.println(RandomStringUtils.randomAlphabetic(2, 10)); // string aléatoire
}
}
3.17 Demo application: data extraction and cleaning
Context: Extract city populations from a multi-line CSV string, cleaning heterogeneous formats (37000000, 20_000_000, 11,000,000), and return them as List<String>.
Two approaches:
Approach 1 — Classic loop:
text.split("\n")→ row array.- For each line:
line.split(",")[1]→ extract the second column. population.replaceAll("[^\\d]", "")→ remove everything that is not a number.- Add to list.
Approach 2 — Modern Stream:
text.lines()→ line stream..map(line -> line.split(",")[1])→ extract the second column..map(pop -> pop.replaceAll("[^\\d]", ""))→ clean..toList()→ collect.
// Fichier : m2strings/src/main/java/strings/ProgramDemo.java
package strings;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class ProgramDemo {
static String text = """
Tokyo, 37000000
New York, 20_000_000
Paris, 11.000.000
""";
public static void main(String[] args) {
// Approche 1 : boucle classique
List<String> populations = new ArrayList<>();
String[] lines = text.split("\n");
for (String line : lines) {
String population = line.split(",")[1];
String sanitized = population.replaceAll("[^\\d]", "");
populations.add(sanitized);
}
System.out.println(populations);
// Approche 2 : Stream moderne
List<String> populations2 = text.lines()
.map(line -> line.split(",")[1])
.map(population -> population.replaceAll("[^\\d]", ""))
.toList();
System.out.println(populations2);
// Approche 3 : avec Function composée
Function<String, String> extractSecondToken = line -> line.split(",")[1];
Function<String, String> sanitizeNumber = numberAsString -> numberAsString.replaceAll("[^\\d]", "");
List<String> populations3 = text.lines()
.map(extractSecondToken.andThen(sanitizeNumber))
.toList();
}
}
4. Working with numbers
Module 4 — 39m 33s
4.1 Introduction — primitive types and wrapper classes
Reminder: Java has primitive types (int, double, float, long, boolean, etc.) and their corresponding wrapper classes (Integer, Double, Float, Long, Boolean, etc.).
Use of wrapper classes:
- Store numbers in collections (eg:
List<Integer>— we cannot doList<int>). - Access useful methods (eg:
Integer.toBinaryString(5)→"101"). - Access min/max constants (
Integer.MIN_VALUE,Integer.MAX_VALUE). - Boxing/Unboxing: automatic conversion between primitive and wrapper.
// Fichier : m3numbers/src/main/java/numbers/WrapperClassDemo.java
package numbers;
public class WrapperClassDemo {
public static void main(String[] args) {
int five = 5;
Integer wrappedFive = Integer.valueOf(5);
System.out.println(five == wrappedFive); // true (grâce au boxing/unboxing)
int min = Integer.MIN_VALUE; // -2147483648
int max = Integer.MAX_VALUE; // 2147483647
System.out.println(min);
System.out.println(max);
System.out.println(Integer.toBinaryString(5)); // "101"
}
}
4.2 Convert a String to a number
Two main methods:
Integer.valueOf(str)→ returns anIntegerwrapper (useful for collections).Integer.parseInt(str)→ returns anintprimitive (default choice).
Equivalents exist for all types: Double.valueOf(), Double.parseDouble(), Long.parseLong(), etc.
// Fichier : m3numbers/src/main/java/numbers/fromstring/ConvertStringToNumber.java
package numbers.fromstring;
import java.util.List;
public class ConvertStringToNumber {
public static void main(String[] args) {
String num = "5";
List<Integer> ints = List.of(Integer.valueOf(num)); // wrapper — pour les collections
int primitiveNum = Integer.parseInt(num); // primitif — choix général
}
}
4.3 Check if a String is a number
Problem: An external input may contain an invalid value → NumberFormatException if trying to convert it directly.
Three approaches:
1 — Loop + Character.isDigit(): Simple, but does not handle signs (+/-), the decimal point, nor scientific notation.
2 — Try-catch on Double.parseDouble(): The most robust method — handles signs, decimals, scientific notation (3.30e23), and D/F suffixes.
3 — NumberUtils.isCreatable() (Apache Commons): Library solution, requires dependency.
// Fichier : m3numbers/src/main/java/numbers/fromstring/CheckStringIsNumber.java
package numbers.fromstring;
import org.apache.commons.lang3.math.NumberUtils;
public class CheckStringIsNumber {
public static void main(String[] args) {
System.out.println(isNumericLoop("5")); // true
System.out.println(isNumericLoop("5a")); // false
System.out.println(isNumericTryCatch("5")); // true
System.out.println(isNumericTryCatch("-5")); // true
System.out.println(isNumericTryCatch("5.5")); // true
System.out.println(isNumericTryCatch("3.30e23")); // true
System.out.println(isNumericTryCatch("3.3f")); // true
System.out.println(NumberUtils.isCreatable("your num")); // false
System.out.println(isNumericTryCatch("5,5")); // false
}
public static boolean isNumericLoop(String str) {
for (char c : str.toCharArray()) {
if (!Character.isDigit(c)) return false;
}
return true;
}
public static boolean isNumericTryCatch(String str) {
try {
Double.parseDouble(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}
4.4 Formatting numbers (DecimalFormat)
DecimalFormat: Allows you to define a precise formatting pattern.
Pattern symbols:
#: digit (omitted if absent).0: digit (forced, displayed as 0 if absent)..: decimal separator.,: thousands separator (grouping).
// Fichier : m3numbers/src/main/java/numbers/formatting/FormattingDoubles.java
package numbers.formatting;
import java.text.DecimalFormat;
import java.text.NumberFormat;
public class FormattingDoubles {
public static void main(String[] args) {
double myDouble = 1345.9375d;
NumberFormat numberFormatter = new DecimalFormat("#.00");
System.out.println("Plain DecimalFormat: " + numberFormatter.format(myDouble));
// "1345.94" (arrondi à 2 décimales)
NumberFormat numberFormatter2 = new DecimalFormat("00,000.0000000");
System.out.println("Another format:" + numberFormatter2.format(myDouble));
// "01,345.9375000" (zéros de tête, séparateur de milliers, 7 décimales)
}
}
4.5 Localized and compact formatting (NumberFormat, Locale)
Problem: Number formatting varies by country. In Germany, the period and comma are reversed compared to the United States (1,345.94 vs 1,345.94).
NumberFormat.getInstance(locale): Formats according to the locale.
Factory methods:
getInstance()orgetNumberInstance(): general decimal format.getPercentInstance(): adds the percent sign.getIntegerInstance(): rounds to the nearest integer.getCompactNumberInstance(locale, style): compact format (Java 12+) — ex:1,000,000→"1 million".
// Fichier : m3numbers/src/main/java/numbers/formatting/LocaleAndCompactFormatDoubles.java
package numbers.formatting;
import java.text.NumberFormat;
import java.util.Locale;
public class LocaleAndCompactFormatDoubles {
public static void main(String[] args) {
double myDouble = 1345.9375d;
System.out.println("Current Locale: " + NumberFormat.getInstance().format(myDouble));
System.out.println("German Locale: " + NumberFormat.getInstance(Locale.GERMAN).format(myDouble));
// Différence : point et virgule inversés
System.out.println("Italian Locale percent: " + NumberFormat.getPercentInstance(Locale.ITALIAN).format(myDouble));
System.out.println("French Locale integer: " + NumberFormat.getIntegerInstance(Locale.FRENCH).format(myDouble));
// Format compact (Java 12+)
NumberFormat formatter = NumberFormat.getCompactNumberInstance(Locale.FRENCH, NumberFormat.Style.LONG);
NumberFormat formatter2 = NumberFormat.getCompactNumberInstance(Locale.KOREAN, NumberFormat.Style.SHORT);
System.out.println(formatter.format(1_000_000)); // "1 million"
System.out.println(formatter2.format(1_000_000)); // format coréen compact
}
}
4.6 Compare integers (Integer wrapper and JVM cache)
Rule:
- Primitive types: use
==. - Wrapper types: use
equals()orcompareTo().
JVM cache trap: By default, Java caches Integer between -128 and 127. For these values, Integer.valueOf(x) == Integer.valueOf(x) is true. Beyond 127, separate objects are created → == returns false.
// Fichier : m3numbers/src/main/java/numbers/comparing/ComparingWholeNumbers.java
package numbers.comparing;
public class ComparingWholeNumbers {
public static void main(String[] args) {
System.out.println(127 == 127); // true (primitifs)
System.out.println(128 == 128); // true (primitifs)
System.out.println(Integer.valueOf("127") == Integer.valueOf("127")); // true (cache JVM)
System.out.println(Integer.valueOf("128") == Integer.valueOf("128")); // FALSE (pas en cache !)
System.out.println(Integer.valueOf("128").equals(Integer.valueOf("128"))); // true
System.out.println(Integer.valueOf("128").compareTo(Integer.valueOf("128"))); // 0
}
}
4.7 Floating point arithmetic (BigDecimal)
Fundamental problem: The binary representation of decimal numbers is approximate. It is impossible to store infinitely many floating point numbers in finite memory.
0.1 + 0.2 == 0.3 // false !
// 0.1 + 0.2 = 0.30000000000000004
Solutions:
1 — Pre-rounding: Acceptable if exact precision is not critical.
2 — Tolerance (epsilon):
double epsilon = 0.00000001;
if (Math.abs(0.3 - (0.1 + 0.2)) < epsilon) { /* "égaux" */ }
Faster (primitive types), but requires setting the appropriate tolerance.
3 — BigDecimal: The most precise solution for financial and other calculations where accuracy is critical.
- Use
new BigDecimal("0.1")(with a string) and nonew BigDecimal(0.1)(with a double — imprecision is preserved). - Methods:
add(),subtract(),multiply(),divide().
// Fichier : m3numbers/src/main/java/numbers/comparing/ComparingDecimalNumbers.java
package numbers.comparing;
import java.math.BigDecimal;
public class ComparingDecimalNumbers {
public static void main(String[] args) {
System.out.println(0.3 == 0.3); // true
System.out.println(0.1 + 0.2 == 0.3); // false !
System.out.println(0.1 + 0.2); // 0.30000000000000004
// Solution 2 : tolérance epsilon
double epsilon = 0.00000001;
if (Math.abs(0.3 - (0.1 + 0.2)) < epsilon) {
System.out.println("true");
}
// Solution 3 : BigDecimal
var num1 = new BigDecimal("0.1");
var num2 = new BigDecimal("0.2");
System.out.println(num1.add(num2).equals(new BigDecimal("0.3"))); // true
}
}
4.8 Very large numbers (BigInteger, BigDecimal)
Native type limitations:
int: max2,147,483,647(~2 billion).long: max9 223 372 036 854 775 807(~9 quintillion).
To go beyond: Use BigInteger or BigDecimal.
// Fichier : m3numbers/src/main/java/numbers/bigdecimal/BigNumbers.java
package numbers.bigdecimal;
import java.math.BigDecimal;
import java.math.BigInteger;
public class BigNumbers {
public static void main(String[] args) {
int maxInt = 2147483647;
long maxLong = 9223372036854775807L;
BigInteger bi = new BigInteger("922337203685477580888");
BigDecimal bd = new BigDecimal("5555555555555555555555555555555555555555.5");
System.out.println(bi);
System.out.println(bd);
bd.add(new BigDecimal("1")); // BigDecimal est immuable !
}
}
Important: BigDecimal (like String) is immutable. bd.add(new BigDecimal("1")) does not modify bd — the result must be affected: bd = bd.add(new BigDecimal("1")).
4.9 Rounding with the Math class
To avoid: The implicit cast (int) 4.6 == 4 — unintentional truncation, difficult to read.
Methods of the Math class:
Math.round(x): standard rounding (≥ 0.5 → up, < 0.5 → down).Math.ceil(x): always upwards (towards +∞).ceil(-4.1) == -4.0.Math.floor(x): always downwards (towards -∞).floor(-4.9) == -5.0.
Results with negative numbers (important):
Math.ceil(-4.1)=-4.0(towards +∞ = minus negative).Math.floor(-4.9)=-5.0(towards -∞ = more negative).Math.round(-1.1)=-1.Math.round(-2.5)=-2(towards +∞ in case of equality — notable asymmetry).
// Fichier : m3numbers/src/main/java/numbers/bigdecimal/RoundingModes.java
package numbers.bigdecimal;
public class RoundingModes {
public static void main(String[] args) {
// À éviter
int truncated = (int) 4.6;
System.out.println(truncated); // 4 — troncature implicite
System.out.println("Ceil-floor");
System.out.println(Math.ceil(4.1)); // 5.0
System.out.println(Math.floor(4.9)); // 4.0
System.out.println(Math.ceil(-4.1)); // -4.0 (vers +∞)
System.out.println(Math.floor(-4.9)); // -5.0 (vers -∞)
System.out.println("Round()");
System.out.println(Math.round(4.5)); // 5
System.out.println(Math.round(4.4)); // 4
System.out.println(Math.round(-1.1)); // -1
System.out.println(Math.round(-2.5)); // -2
}
}
4.10 Rounding with DecimalFormat and BigDecimal
Default behavior of DecimalFormat — Bank rounding (HALF_EVEN):
- If equidistant, rounds towards even neighbor.
1.5→2;2.5→2;3.5→4;4.5→4.- Avoids statistical bias during extensive rounding.
Change rounding mode:
df.setRoundingMode(RoundingMode.HALF_UP); // comportement "scolaire"
RoundingMode (enum): HALF_EVEN, HALF_UP, HALF_DOWN, CEILING, FLOOR, UP, DOWN, UNNECESSARY.
With BigDecimal.setScale():
BigDecimal.valueOf(1.5).setScale(2, RoundingMode.HALF_UP) // 1.50
// Fichier : m3numbers/src/main/java/numbers/bigdecimal/RoundingWithDecimalFormatAndBigDecimal.java
package numbers.bigdecimal;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.NumberFormat;
public class RoundingWithDecimalFormatAndBigDecimal {
public static void main(String[] args) {
System.out.println("DecimalFormat");
NumberFormat df = new DecimalFormat("0");
System.out.println(df.format(1.4)); // 1
System.out.println(df.format(1.6)); // 2
// Arrondi bancaire (défaut) : les deux arrondissent à 2
System.out.println(df.format(1.5)); // 2
System.out.println(df.format(2.5)); // 2
df.setRoundingMode(RoundingMode.HALF_UP);
System.out.println(df.format(2.5)); // 3 (HALF_UP)
System.out.println("BigDecimal");
System.out.println(BigDecimal.valueOf(1.5).setScale(2, RoundingMode.HALF_UP)); // 1.50
System.out.println(BigDecimal.valueOf(2.5).setScale(2, RoundingMode.HALF_UP)); // 2.50
}
}
4.11 Generate random values (ThreadLocalRandom)
Since Java 7: ThreadLocalRandom (recommended):
- Produces higher quality random numbers than
Random(more uniform distribution). - Very fast and thread safe.
- Source: Effective Java by Joshua Bloch.
Old Random method (historical, for recognition):
// Fichier : m3numbers/src/main/java/numbers/random/RandomValueMain.java
package numbers.random;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public class RandomValueMain {
public static void main(String[] args) {
// Ancienne méthode (à reconnaître dans du code existant)
Random random = new Random();
System.out.println(random.nextInt());
System.out.println(random.nextInt(5)); // [0, 5)
System.out.println(random.nextInt(7, 11)); // [7, 11)
System.out.println(random.nextBoolean());
System.out.println(random.nextDouble());
// Méthode recommandée (Java 7+)
var threadSafeRandom = ThreadLocalRandom.current();
System.out.println(threadSafeRandom.nextInt());
System.out.println(threadSafeRandom.nextInt(5, 10)); // [5, 10)
System.out.println(threadSafeRandom.nextDouble());
}
}
4.12 Demo application 1: convert a list of formatted numbers
Scenario: Starting from a list of populations in the form of strings ("37000000", …) and displaying them in a compact and localized format (eg: "37 Millionen" in German).
Steps:
- Create a
NumberFormat.getCompactNumberInstance(locale, style). - Stream the list of strings.
.map(Integer::parseInt)→ convert to integers..map(formatter::format)→ apply format.- Collect into list.
Extension: Accept locale and style as program arguments (args[0], args[1]).
// Fichier : m3numbers/src/main/java/numbers/ProgramDemo.java
package numbers;
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
public class ProgramDemo {
public static void main(String[] args) {
var locale = Locale.forLanguageTag(args[0]);
var style = NumberFormat.Style.valueOf(args[1]);
List<String> nums = List.of("37000000", "20000000", "11000000");
var formatter = NumberFormat.getCompactNumberInstance(locale, style);
List<String> formattedNums = nums.stream()
.map(Integer::parseInt)
.map(formatter::format)
.toList();
System.out.println(formattedNums);
// Exemple avec "de" et "LONG" : [37 Millionen, 20 Millionen, 11 Millionen]
}
}
4.13 Demo application 2: rounding and calculating VAT
Challenge: Transform a price list by adding 20% VAT, then round to 2 decimal places with HALF_UP.
Steps:
Double::parseDouble→ convert strings to doubles.price -> price * 1.2→ add 20%.formatter::format→ format withDecimalFormat("#.00")andRoundingMode.HALF_UP.
// Fichier : m3numbers/src/main/java/numbers/ProgramDemoTwo.java
package numbers;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.List;
public class ProgramDemoTwo {
public static void main(String[] args) {
// Tâche : ajouter +20% et arrondir à 2 décimales → 24.18, 44.66, 69.83
List<String> prices = List.of("20.15", "37.22", "58.19");
var formatter = new DecimalFormat("#.00");
formatter.setRoundingMode(RoundingMode.HALF_UP);
List<String> pricesWithVat = prices.stream()
.map(Double::parseDouble)
.map(price -> price * 1.2)
.map(formatter::format)
.toList();
System.out.println(pricesWithVat); // [24.18, 44.66, 69.83]
}
}
5. Solve tasks with dates and times
Module 5 — 44m 6s
5.1 Introduction
Dates and times are present in almost all applications: getting the current timestamp, checking if a date is before or after a deadline, calculating ages, etc. This module covers essential recipes, from the simplest to the most complex.
Progress: Dates only → Dates + times → Time zones.
5.2 Date-Time API Overview
The Java Date-Time API (introduced in Java 8):
| Class | Represents | Use cases |
|---|---|---|
LocalDate | A date without time or zone | Birthdays, dates of birth |
LocalTime | An hour without date or time zone | Meeting of the day |
LocalDateTime | Date + time, without time zone | Local meetings |
ZoneId | Time zone identifier | Ex: "America/New_York" |
ZonedDateTime | Date + time + time zone | International meetings, flights |
Instant | Point on the UTC timeline (since epoch 1970) | System Timestamps |
Instant.now().toEpochMilli() ≡ System.currentTimeMillis().
// Fichier : m4datetime/src/main/java/overview/ApiOverview.java
package overview;
import java.time.*;
public class ApiOverview {
public static void main(String[] args) {
System.out.println(LocalDate.now());
System.out.println(LocalTime.now());
System.out.println(LocalDateTime.now());
System.out.println(Instant.now());
System.out.println(Instant.now().toEpochMilli());
System.out.println(System.currentTimeMillis());
System.out.println(ZonedDateTime.now());
}
}
5.3 Calculate the difference between two dates (Period)
Use case: Calculate the age of a person or object.
Period.between(start, end): Returns a Period object.
getYears(),getMonths(),getDays(): return the remaining components (ex: 13 months → 1 year + 1 month).
Important: These methods return the separate components, not the running total. For the total: use ChronoUnit.YEARS.between(), etc. (see section 5.8).
// Fichier : m4datetime/src/main/java/dates/DifferenceBetweenDatesDemo.java
package dates;
import java.time.LocalDate;
import java.time.Period;
public class DifferenceBetweenDatesDemo {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2010, 11, 2);
LocalDate today = LocalDate.now();
Period age = Period.between(start, today);
System.out.println(age.getYears() + "y "
+ age.getMonths() + "m "
+ age.getDays() + "d");
if (age.getYears() < 18) {
System.out.println("Sorry, you can't buy this age-restricted item");
}
}
}
5.4 Compare two dates (isBefore, isAfter)
Methods: isBefore(), isAfter(), isEqual() — all return a boolean.
// Fichier : m4datetime/src/main/java/dates/CompareDatesDemo.java
package dates;
import java.time.LocalDate;
public class CompareDatesDemo {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2021, 12, 30);
LocalDate now = LocalDate.now();
System.out.println(date.isAfter(now) ? "Not Yet!" : "Already past!");
}
}
5.5 First or last day of a period (TemporalAdjusters)
TemporalAdjusters (class, plural): Provides static methods to obtain TemporalAdjusters (date calculation strategies).
Available methods: firstDayOfMonth(), lastDayOfMonth(), firstDayOfYear(), lastDayOfYear(), nextOrSame(DayOfWeek), next(DayOfWeek), previous(DayOfWeek), etc.
Usage: today.with(TemporalAdjusters.lastDayOfMonth()).
// Fichier : m4datetime/src/main/java/dates/StartEndOfPeriodDemo.java
package dates;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class StartEndOfPeriodDemo {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate endOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
System.out.println(today);
System.out.println(endOfMonth);
String message = String.format("Hurry! The sale ends at the end of the month, %s day(s) remaining!",
endOfMonth.getDayOfMonth() - today.getDayOfMonth());
System.out.println(message);
}
}
5.6 Manage recurring events
Use case: Show all Friday meetings through the end of the year.
Strategy:
- Calculate the start and end of the period (
firstDayOfMonth,lastDayOfYear). - Find the first Friday with
nextOrSame(DayOfWeek.FRIDAY). whileloop: add to the list, then advance one week withplusWeeks(1).
// Fichier : m4datetime/src/main/java/dates/HandlingRecurringEvents.java
package dates;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
public class HandlingRecurringEvents {
public static void main(String[] args) {
var today = LocalDate.now();
var start = today.with(TemporalAdjusters.firstDayOfMonth());
var stop = today.with(TemporalAdjusters.lastDayOfYear());
var firstFriday = start.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
System.out.println("Start date " + start);
System.out.println("End date " + stop);
List<LocalDate> fridays = new ArrayList<>();
while (firstFriday.isBefore(stop) || firstFriday.isEqual(stop)) {
fridays.add(firstFriday);
firstFriday = firstFriday.plusWeeks(1);
}
System.out.printf("From %s to %s, Fridays will be on %s%n", start, stop, fridays);
}
}
5.7 Add or subtract dates
Methods on LocalDate: plusDays(), plusWeeks(), plusMonths(), plusYears(), minusDays(), minusWeeks(), etc.
Generic method plus(amount, ChronoUnit):
date.plus(1, ChronoUnit.DECADES);
date.plus(1, ChronoUnit.CENTURIES);
Important: LocalDate has no concept of time. Adding minutes will throw an UnsupportedTemporalTypeException. Use LocalDateTime for time units (hours, minutes, seconds).
With Period.of(years, months, days): For composite periods.
// Fichier : m4datetime/src/main/java/dates/AddSubtractPeriods.java
package dates;
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class AddSubtractPeriods {
public static void main(String[] args) {
var date = LocalDate.now();
System.out.println(date.plusDays(1));
System.out.println(date.plusWeeks(1));
System.out.println(date.plusMonths(3));
System.out.println(date.plusYears(5));
System.out.printf("Your scheduled appointment will take place in %s days",
date.plusMonths(1).getDayOfYear() - date.getDayOfYear());
System.out.println(date.plus(1, ChronoUnit.DECADES));
// date.plus(1, ChronoUnit.MINUTES) → UnsupportedTemporalTypeException !
Period period = Period.of(0, 1, 5); // 0 ans, 1 mois, 5 jours
System.out.println(date.plus(period));
}
}
5.8 Demo application 1: time remaining in the year
Challenge: Display the number of whole months, whole weeks and whole days remaining until the end of the year.
First (erroneous) attempt with Period:
period.get(ChronoUnit.WEEKS)throws anUnsupportedTemporalTypeExceptionbecause a 7 day week is not universal across all calendar systems.period.getDays()returns the remaining days after deducting months, not the total.
Correct solution with date.until(endDate, ChronoUnit):
// Fichier : m4datetime/src/main/java/demoapp/DemoAppOne.java
package demoapp;
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
public class DemoAppOne {
public static void main(String[] args) {
var today = LocalDate.now();
var lastDay = today.with(TemporalAdjusters.lastDayOfYear());
// 1re tentative (résultats incorrects)
var period = Period.between(today, lastDay);
System.out.printf("Only %s months left till EOY", period.getMonths());
System.out.printf("\nOnly %s days left till EOY", period.getDays()); // jours résiduels seulement
// 2e tentative (correcte)
System.out.printf("\nOnly %s whole months left till EOY", today.until(lastDay, ChronoUnit.MONTHS));
System.out.printf("\nOnly %s whole weeks left till EOY", today.until(lastDay, ChronoUnit.WEEKS));
System.out.printf("\nOnly %s whole days left till EOY", today.until(lastDay, ChronoUnit.DAYS));
}
}
5.9 Format dates (DateTimeFormatter)
Three approaches to create a DateTimeFormatter:
1 — Predefined constants:
DateTimeFormatter.ISO_DATE // "2022-10-31"
DateTimeFormatter.BASIC_ISO_DATE // "20221031"
DateTimeFormatter.ISO_DATE_TIME // nécessite un ZonedDateTime
2 — Factory methods localized:
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL) // "Monday, October 31, 2022"
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) // "10/31/22"
.withLocale(new Locale("fr", "FR")) // "31/10/22"
3 — Custom pattern:
DateTimeFormatter.ofPattern("dd-MM-yyyy") // "31-10-2022"
DateTimeFormatter.ofPattern("MM.dd.yyyy") // "10.31.2022"
Frequent error: UnsupportedTemporalTypeException — the formatter expects more information than the object provided (eg: a formatter with time zone applied to a LocalDateTime).
// Fichier : m4datetime/src/main/java/dates/FormatDatesDemo.java
package dates;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
public class FormatDatesDemo {
public static void main(String[] args) {
var date = LocalDateTime.now();
System.out.println(date);
var isoDate = DateTimeFormatter.ISO_DATE.format(date);
System.out.println(isoDate);
var basicIsoDate = DateTimeFormatter.BASIC_ISO_DATE.format(date);
System.out.println(basicIsoDate);
var someDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(date);
System.out.println(someDate);
var someDate3 = DateTimeFormatter
.ofLocalizedDate(FormatStyle.SHORT).withLocale(new Locale("en", "US")).format(date);
System.out.println(someDate3); // "10/31/22"
var someDate4 = DateTimeFormatter
.ofLocalizedDate(FormatStyle.SHORT).withLocale(new Locale("fr", "FR")).format(date);
System.out.println(someDate4); // "31/10/22"
String europeanPattern = "dd-MM-yyyy";
String usPattern = "MM.dd.yyyy";
var fixedDate = LocalDate.of(2022, 10, 31);
System.out.println(DateTimeFormatter.ofPattern(europeanPattern).format(fixedDate)); // 31-10-2022
System.out.println(DateTimeFormatter.ofPattern(usPattern).format(fixedDate)); // 10.31.2022
}
}
5.10 Handle multiple date formats
Problem: An entry can contain dates in several different formats. A single formatter will throw an exception for unrecognized formats.
Solution: DateTimeFormatterBuilder.appendOptional():
// Fichier : m4datetime/src/main/java/dates/MultipleFormatsDemo.java
package dates;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
public class MultipleFormatsDemo {
public static void main(String[] args) {
var formatBuilder = new DateTimeFormatterBuilder();
formatBuilder.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
.appendOptional(DateTimeFormatter.ofPattern("MM-dd-yyyy"))
.appendOptional(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
var formatter = formatBuilder.toFormatter();
System.out.println(LocalDate.parse("2022-07-22", formatter)); // OK
System.out.println(LocalDate.parse("22-07-2022", formatter)); // OK
System.out.println(LocalDate.parse("07-22-2022", formatter)); // ERREUR → DateTimeParseException
}
}
Beware of conflicting patterns: Java does not check patterns that are mutually exclusive. It prioritizes the first added pattern that matches.
5.11 Convert from/to old Date
Context: Sometimes external APIs return a java.util.Date (legacy) which must be converted to a modern type.
Date → LocalDateTime:
LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());
LocalDateTime → Date:
Date out = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
// Fichier : m4datetime/src/main/java/dates/ConvertOldDateDemo.java
package dates;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
public class ConvertOldDateDemo {
public static void main(String[] args) {
Date in = new Date(); // depuis une API externe
LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());
LocalDateTime ldt2 = LocalDateTime.ofInstant(in.toInstant(), ZoneId.of("America/New_York"));
Date out = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
System.out.println(in);
System.out.println(ldt);
System.out.println(ldt2);
System.out.println(out); // identique à in
}
}
5.12 Get current time (Instant, LocalDateTime, ZoneId)
Subtlety: Instant.now() always returns UTC time. LocalDateTime.now() returns the local system time.
For a specific timezone:
LocalDateTime.now(ZoneId.of("America/New_York"))
Important for international code: Consider the impact of time zone on timestamps. Code that works well in Europe may produce different results if run in the United States or Asia.
// Fichier : m4datetime/src/main/java/datetime/CurrentTimeMain.java
package datetime;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
public class CurrentTimeMain {
public static void main(String[] args) {
System.out.println(Instant.now()); // UTC
System.out.println(LocalDateTime.now()); // heure locale
System.out.println(LocalDateTime.now(ZoneId.of("America/New_York"))); // heure NY
}
}
5.13 List all ZoneId
ZoneId.getAvailableZoneIds(): Returns a Set<String> of all valid time zone identifiers.
// Fichier : m4datetime/src/main/java/datetime/GetAllZoneIds.java
package datetime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class GetAllZoneIds {
public static final boolean SORT_BY_REGION = true;
public static void main(String[] args) {
// Liste brute
ZoneId.getAvailableZoneIds().forEach(System.out::println);
// Liste triée et formatée (triée par UTC ou par région)
Map<String, String> sortedMap = new LinkedHashMap<>();
Map<String, String> allZoneIdsAndItsOffSet = getAllZoneIdsAndItsOffSet();
if (SORT_BY_REGION) {
allZoneIdsAndItsOffSet.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(entry -> sortedMap.put(entry.getKey(), entry.getValue()));
} else {
allZoneIdsAndItsOffSet.entrySet().stream()
.sorted(Map.Entry.<String, String>comparingByValue().reversed())
.forEachOrdered(entry -> sortedMap.put(entry.getKey(), entry.getValue()));
}
sortedMap.forEach((k, v) -> System.out.printf("%35s (UTC%s) %n", k, v));
}
private static Map<String, String> getAllZoneIdsAndItsOffSet() {
Map<String, String> result = new HashMap<>();
LocalDateTime localDateTime = LocalDateTime.now();
for (String zoneId : ZoneId.getAvailableZoneIds()) {
ZoneId id = ZoneId.of(zoneId);
String offset = localDateTime.atZone(id)
.getOffset().getId().replaceAll("Z", "+00:00");
result.put(id.toString(), offset);
}
return result;
}
}
5.14 Start and end of day (LocalTime.MAX)
Use case: Show time remaining until midnight (“Sale ends at midnight!”).
LocalTime constants: LocalTime.MAX (23:59:59.999999999), LocalTime.MIDNIGHT (00:00), LocalTime.NOON (12:00).
// Fichier : m4datetime/src/main/java/datetime/StartEndDay.java
package datetime;
import java.time.*;
public class StartEndDay {
public static void main(String[] args) throws InterruptedException {
while (true) { // boucle infinie — démo seulement !
Thread.sleep(1000);
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime nowToday = LocalDateTime.of(today, now);
LocalDateTime midnightToday = LocalDateTime.of(today, LocalTime.MAX);
String message = String.format("Hurry, sale ends at midnight, time left: %s hours, %s minutes, %s seconds",
midnightToday.getHour() - nowToday.getHour(),
midnightToday.getMinute() - nowToday.getMinute(),
midnightToday.getSecond() - nowToday.getSecond());
System.out.println(message);
}
}
}
5.15 Calculate arrival time in another time zone
Use case: London → New York flight. Departure at 7 a.m., duration 8 hours → arrival at what local time in New York?
Steps:
- Create start (
LocalDateTime.of(...)). - Add the duration (
plusHours(8)). - Attach a zone:
departure.atZone(ZoneId.of("Europe/London"))→ZonedDateTime. - Convert to destination zone:
arrivalLondon.withZoneSameInstant(ZoneId.of("America/New_York")).
withZoneSameInstant() vs withZoneSameLocal():
withZoneSameInstant(): converts the same instant into another time zone (what we need here).withZoneSameLocal(): keeps the same local time but changes the zone (rarely useful).
// Fichier : m4datetime/src/main/java/datetime/CalculateArrivalTime.java
package datetime;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class CalculateArrivalTime {
public static void main(String[] args) {
var departure = LocalDateTime.of(2022, Month.JULY, 22, 7, 0);
var arrival = departure.plusHours(8);
var departureLondonPerspective = departure.atZone(ZoneId.of("Europe/London"));
var arrivalLondonPerspective = arrival.atZone(ZoneId.of("Europe/London"));
System.out.println(departureLondonPerspective); // 07:00 BST
System.out.println(arrivalLondonPerspective); // 15:00 BST
var departureNyPerspective = departureLondonPerspective
.withZoneSameInstant(ZoneId.of("America/New_York"));
var arrivalNyPerspective = arrivalLondonPerspective
.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println(departureNyPerspective); // 02:00 EDT
System.out.println(arrivalNyPerspective); // 10:00 EDT
// Record Flight pour généraliser
System.out.println(new Flight("Europe/London", "America/New_York", departure, 8));
System.out.println(new Flight("Europe/Berlin", "Asia/Dubai", departure, 4));
}
public record Flight(String from, String to, LocalDateTime departure, int duration) {
@Override
public String toString() {
ZoneId fromZone = ZoneId.of(from);
ZoneId toZone = ZoneId.of(to);
var departTime = departure.atZone(fromZone);
var arrivalTime = departure.plusHours(duration)
.atZone(fromZone).withZoneSameInstant(toZone);
return String.format("Flight departs at %s and arrives at %s%n", departTime, arrivalTime);
}
}
}
5.16 Demo application 2: current time in other time zones
Scenario: Given a local time, display the corresponding time in other time zones (like WorldTimeBuddy).
Strategy:
- Enrich
LocalTimewith the date and the current zone →ZonedDateTime. - For each other zone:
zonedDateTime.withZoneSameInstant(otherZone).
// Fichier : m4datetime/src/main/java/demoapp/DemoAppTwo.java
package demoapp;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class DemoAppTwo {
public static void main(String[] args) {
LocalTime time = LocalTime.now();
List<String> zones = List.of("America/New_York", "Europe/London", "Asia/Kolkata");
printTimes(time, zones);
}
static void printTimes(LocalTime time, List<String> otherZoneIDs) {
ZonedDateTime dateTime = time.atDate(LocalDate.now()).atZone(ZoneId.systemDefault());
System.out.println(dateTime.getZone() + " " + dateTime.format(DateTimeFormatter.ISO_LOCAL_TIME));
System.out.println("===================");
for (var zone : otherZoneIDs) {
ZonedDateTime otherTime = dateTime.withZoneSameInstant(ZoneId.of(zone));
System.out.println(zone + " " + otherTime.format(DateTimeFormatter.ISO_LOCAL_TIME));
}
}
}
6. Working with arrays and collections
Module 6 — 1h 0m 33s
6.1 Introduction
This module focuses on practical and common tasks with tables and collections: adding, deleting, replacing, sorting, finding duplicates, etc. It does not address:
- Internal differences between implementations (ArrayList vs LinkedList, etc.) — see dedicated courses.
- Data structures and algorithms in depth.
Reminder of the Java collection hierarchy:
Iterable→Collection→List,Queue,Set(with their implementations).Map(separate interface) →HashMap,LinkedHashMap,TreeMap.
6.2 Add an element to an array (Arrays.copyOf)
Reminder: Java arrays have a fixed size. You cannot add elements directly.
Solution: Create a larger copy with Arrays.copyOf(), then add the element at the end.
// Fichier : m5collections/src/main/java/array/AddElementToArray.java
package array;
import java.util.Arrays;
public class AddElementToArray {
public static void main(String[] args) {
String[] arr = {"1", "2", "3"};
arr[2] = "10"; // modifier à index existant → OK
System.out.println(Arrays.toString(arr));
String[] arr2 = append(arr, "4");
System.out.println(Arrays.toString(arr2));
}
static <T> T[] append(T[] sourceArray, T newElement) {
int origLength = sourceArray.length;
T[] newArray = Arrays.copyOf(sourceArray, origLength * 2); // doubler la taille
newArray[origLength] = newElement;
return newArray;
}
}
Note: The method uses generics (<T>) to work with any object type. Doubling the size is a common strategy (like ArrayList internally).
6.3 Convert between array and List
From table to an editable List:
List<String> modifiable = new ArrayList<>(List.of(array)); // recommandé
// OU
List<String> modifiable = new ArrayList<>(Arrays.asList(array)); // héritage
From a List to an array:
String[] array = list.toArray(String[]::new);
Trap with primitives: An int[] cannot not be converted directly to List<Integer> (autoboxing does not work with arrays). Solution: use streams.
List<Integer> intList = Arrays.stream(ints).boxed().toList();
// Fichier : m5collections/src/main/java/array/ConvertToFromArray.java
package array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ConvertToFromArray {
public static void main(String[] args) {
String[] stringsArr = {"1", "2", "3"};
List<String> stringList = new ArrayList<>(List.of(stringsArr));
System.out.println("List Before: " + stringList);
stringList.add("4");
stringList.remove("1");
System.out.println("List After: " + stringList);
String[] strings = stringList.toArray(String[]::new);
System.out.println("Back to array: " + Arrays.toString(strings));
// Primitifs → boxing nécessaire
int[] ints = {1, 2, 3, 4, 5};
List<Integer> intList2 = Arrays.stream(ints)
.filter(n -> n < 4)
.boxed()
.toList();
}
}
6.4 Create a List and add elements
Three creation methods:
| Method | Editable | Resizable | Nulls | Notes |
|---|---|---|---|---|
new ArrayList<>(Arrays.asList(...)) | Yes | Yes | Yes | Fully editable |
Arrays.asList(...) | Yes (set) | No | Yes | Not resizable — table view |
List.of(...) | No | No | No | Immutable — to be preferred |
Important: Arrays.asList() creates a view on the underlying array. Modifying the original table also modifies the list. List.of() creates a truly immutable list.
Hint: Prefer List.of() unless there is a reason otherwise. Immutable and null-safe collections prevent a lot of unexpected bugs. If changes are needed, stream and collect.
// Fichier : m5collections/src/main/java/list/CreateListAndAddElement.java
package list;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CreateListAndAddElement {
public static void main(String[] args) {
// Entièrement modifiable
List<String> strings3 = new ArrayList<>(Arrays.asList("a", "b"));
strings3.add("c");
// Non redimensionnable, mais modifiable (set)
List<String> strings2 = Arrays.asList("a", "b", null);
strings2.set(0, "c");
// Immuable — à préférer
List<String> strings = List.of("a", "b");
}
}
6.5 Check if a List is empty (isEmpty)
Use isEmpty() rather than list.size() == 0: More concise, more readable in conditions, and semantically explicit.
// Fichier : m5collections/src/main/java/list/CheckListIsEmpty.java
package list;
import java.util.List;
public class CheckListIsEmpty {
public static void main(String[] args) {
int[] intArray = {};
System.out.println(intArray.length == 0); // tableaux primitifs — .length
List<Integer> intList = List.of(1, 2, 3);
System.out.println(intList.size() == 0); // fonctionne mais verbose
System.out.println(intList.isEmpty()); // préférable
if (intList.isEmpty()) {
System.out.println("Is empty");
}
}
}
6.6 Remove elements from a List (removeIf)
Reminder: You cannot delete from an immutable list (List.of()) → UnsupportedOperationException.
On an editable list — methods:
removeIf(Predicate): removes all elements matching the predicate.removeAll(Collection): removes all elements present in the provided collection.- Also works on custom types (records, classes).
// Fichier : m5collections/src/main/java/list/RemoveElementsOfList.java
package list;
import java.util.ArrayList;
import java.util.List;
public class RemoveElementsOfList {
public static void main(String[] args) {
List<Integer> ints2 = new ArrayList<>(List.of(20, 30, 40, 50));
ints2.removeIf(i -> i < 40);
System.out.println(ints2); // [40, 50]
List<String> strings = new ArrayList<>(List.of("Marie", "Jake", "Jon", "Sarah"));
strings.removeIf(s -> s.endsWith("ie"));
System.out.println(strings); // [Jake, Jon, Sarah]
strings.removeAll(List.of("Jake", "Jon")); // [Sarah]
// Avec des types personnalisés (record)
List<Person> personList = new ArrayList<>(List.of(new Person("Jon"), new Person("Andrew")));
personList.removeIf(p -> p.name().equals("Andrew"));
System.out.println(personList); // [Jon]
}
record Person(String name) {}
}
6.7 Remove duplicates (HashSet)
Simple solution: Convert the list to HashSet (which does not accept duplicates by definition).
With native types (int, String, etc.): it works directly because Java knows their equality.
With custom types: Need to override equals() and hashCode() to define what “identical” means.
// Fichier : m5collections/src/main/java/list/RemoveDuplicates.java
package list;
import java.util.*;
public class RemoveDuplicates {
public static void main(String[] args) {
List<Integer> ints = List.of(20, 30, 40, 40, 50, 50);
Set<Integer> intSet = new HashSet<>(ints);
System.out.println(intSet); // [20, 30, 40, 50]
// Avec type personnalisé — nécessite equals() et hashCode()
List<Person> personList = new ArrayList<>(List.of(new Person("Jon"), new Person("Jon")));
Set<Person> personSet = new HashSet<>(personList);
System.out.println(personSet); // [Jon] — grâce aux overrides
}
static class Person {
String name;
public Person(String name) { this.name = name; }
@Override
public boolean equals(Object obj) {
final Person other = (Person) obj;
return this.name.equals(other.name);
}
@Override
public int hashCode() {
int hash = 3;
hash = 53 * hash + (this.name != null ? this.name.hashCode() : 0);
return hash;
}
@Override
public String toString() { return name; }
}
}
6.8 Find common and different elements in two Lists
Intersection (elements in common): set.retainAll(otherCollection).
Symmetrical difference (single elements in both):
new HashSet<>()to avoid duplicates.addAll(listOne)+addAll(listTwo).removeAll(intersection).
// Fichier : m5collections/src/main/java/list/FindDuplicatesAndSimilarElements.java
package list;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class FindDuplicatesAndSimilarElements {
public static void main(String[] args) {
List<String> listOne = List.of("USA", "Brazil", "USA", "France", "Egypt", "India", "Japan");
List<String> listTwo = List.of("USA", "Brazil", "Germany", "Australia");
// Intersection
Set<String> similar = new HashSet<>(listOne);
similar.retainAll(listTwo);
System.out.println(similar); // [USA, Brazil]
// Éléments uniques (différence symétrique)
Set<String> different = new HashSet<>();
different.addAll(listOne);
different.addAll(listTwo);
different.removeAll(similar);
System.out.println(different); // [France, Egypt, India, Japan, Germany, Australia]
}
}
6.9 Replacing elements in a List (replaceAll)
replaceAll(UnaryOperator<E>): Applies a transform function to each element in the list, modifying the list in place.
UnaryOperator<T>: Functional interface that takes a T and returns a T (same type).
// Fichier : m5collections/src/main/java/list/ReplaceElementInAList.java
package list;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
public class ReplaceElementInAList {
public static void main(String[] args) {
List<Double> doubles = new ArrayList<>(List.of(10.525, 20.567, 30.789));
// Opérateur simple : arrondir tous les nombres
UnaryOperator<Double> operator = num -> BigDecimal.valueOf(num)
.setScale(2, RoundingMode.HALF_EVEN).doubleValue();
// Opérateur conditionnel (ternaire)
UnaryOperator<Double> operator2 = num -> num < 30 ? BigDecimal.valueOf(num)
.setScale(2, RoundingMode.HALF_EVEN).doubleValue() : num;
// Opérateur conditionnel (bloc)
UnaryOperator<Double> operator3 = num -> {
if (num < 30) {
return BigDecimal.valueOf(num).setScale(2, RoundingMode.HALF_EVEN).doubleValue();
}
return num;
};
doubles.replaceAll(operator);
System.out.println(doubles); // [10.52, 20.57, 30.789] (HALF_EVEN → 10.52 pas 10.53)
// Avec strings : capitaliser les noms
List<String> strings = new ArrayList<>(List.of("mary", "jake", "thomas"));
strings.replaceAll(str -> str.substring(0, 1).toUpperCase() + str.substring(1));
System.out.println(strings); // [Mary, Jake, Thomas]
}
}
Note: The bank rounding HALF_EVEN may be surprising. 10.525 rounded to 2 decimal places → 10.52 (not 10.53) because 2 is even.
6.10 Simple sorting of a List (Comparator)
Method list.sort(Comparator):
- For integers:
Integer::compareTo. - For doubles:
Double::compareTo. - For strings:
String::compareTo(lexicographic order, uppercase before lowercase).
Comparator static: Comparator.naturalOrder() (same as compareTo) and Comparator.reverseOrder().
// Fichier : m5collections/src/main/java/list/SortingSimple.java
package list;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class SortingSimple {
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>(List.of(20, 50, 10));
ints.sort(Integer::compareTo);
System.out.println(ints); // [10, 20, 50]
List<Double> doubles = new ArrayList<>(List.of(30.789, 20.551, 20.55));
doubles.sort(Double::compareTo);
System.out.println(doubles);
// Strings — majuscules avant minuscules (code Unicode)
List<String> strings = new ArrayList<>(List.of("andre", "maria", "Andre"));
strings.sort(String::compareTo);
System.out.println(strings); // [Andre, andre, maria]
strings.sort(Comparator.naturalOrder());
System.out.println("Natural order: " + strings);
strings.sort(Comparator.reverseOrder());
System.out.println("Reverse order: " + strings);
}
}
6.11 Advanced sorting with custom types
Problem: personList.sort(String::compareTo) does not compile — type mismatch.
Solution — Comparator.comparing():
personList.sort(Comparator.comparing(Person::age));
personList.sort(Comparator.comparing(Person::name));
Composability:
.reversed(): reverse the order..thenComparing(): secondary sort (if the first fields are equal).Comparator.nullsFirst()/Comparator.nullsLast(): handlenullvalues.
// Fichier : m5collections/src/main/java/list/SortingAdvanced.java
package list;
import java.util.*;
import static java.util.Comparator.*;
public class SortingAdvanced {
public static void main(String[] args) {
List<Person> personList = new ArrayList<>(Arrays.asList(
new Person("Jake", 28),
new Person("Don", 30),
new Person("Andy", 40),
new Person("John", 36),
new Person("John", 35)
));
personList.sort(comparing(Person::age));
System.out.println("Compared by age: " + personList);
personList.sort(comparing(Person::name));
System.out.println("Compared by name: " + personList);
personList.sort(comparing(Person::name).reversed());
System.out.println("Reversed by name: " + personList);
personList.sort(comparing(Person::age).thenComparing(Person::name));
System.out.println("By age then by name: " + personList);
// Gestion des nulls
List<Person> withNulls = new ArrayList<>(Arrays.asList(null, new Person("Jake", 28), null));
withNulls.sort(Comparator.nullsFirst(comparing(Person::name)));
System.out.println(withNulls);
// Avec arrays
Person[] personArr = {null, new Person("Jake", 28), null};
Arrays.sort(personArr, nullsLast(comparing(Person::name).thenComparing(Person::age)));
}
record Person(String name, int age) {
@Override
public String toString() { return name + "=" + age; }
}
}
6.12 Map overview
Reminder: A Map stores data in key-value pairs. Keys are unique; values may be duplicated.
Create an immutable Map:
Map<Integer, String> immutable = Map.of(1, "Java", 2, "C#");
Create a mutable Map:
Map<Integer, String> mutable = new HashMap<>(Map.of(1, "Java", 2, "C#"));
Access methods:
entrySet(): set of key-value pairs (Set<Map.Entry<K,V>>).keySet(): set of keys (Set<K>).values(): collection of values (Collection<V>).
Warning: A duplicate key when creating a Map.of() throws an IllegalArgumentException.
// Fichier : m5collections/src/main/java/map/MapOverview.java
package map;
import java.util.HashMap;
import java.util.Map;
public class MapOverview {
public static void main(String[] args) {
Map<Integer, String> immutable = Map.of(1, "Java", 2, "C#", 3, "JavaScript", 4, "JavaScript");
Map<Integer, String> mutableMap = new HashMap<>(Map.of(1, "Java", 2, "C#", 3, "JavaScript"));
System.out.println(immutable.entrySet());
System.out.println(immutable.keySet());
System.out.println(immutable.values()); // valeurs dupliquées autorisées
for (Map.Entry<Integer, String> entry : immutable.entrySet()) {
if (entry.getKey() < 2) {
String modifiedValue = entry.getValue().toUpperCase();
}
}
}
}
6.13 Find duplicate values in a Map
Test for duplicates:
// Les tailles diffèrent si des doublons ont été supprimés par le Set
return values.size() != new HashSet<>(values).size();
Collect duplicate values:
Use Collections.frequency(collection, element) to count occurrences.
// Fichier : m5collections/src/main/java/map/MapFindDuplicateValues.java
package map;
import java.util.*;
public class MapFindDuplicateValues {
public static void main(String[] args) {
Map<Integer, String> map = Map.of(1, "Java", 2, "C#", 3, "C#", 4, "JavaScript", 5, "JavaScript");
System.out.println(mapHasDuplicates(map)); // true
System.out.println(collectDuplicateValues(map)); // [C#, JavaScript]
}
public static <K, V> boolean mapHasDuplicates(Map<K, V> map) {
Collection<V> valuesList = map.values();
Set<V> valuesSet = new HashSet<>(valuesList);
return valuesList.size() != valuesSet.size();
}
private static <K, V> List<V> collectDuplicateValues(Map<K, V> map) {
return map.values().stream()
.filter(value -> Collections.frequency(map.values(), value) > 1)
.distinct()
.toList();
}
}
6.14 Delete and replace elements in a Map
Deletion:
map.remove(key): removes by key.map.entrySet().removeIf(entry -> ...): conditional removal by value or key.
Replacement:
map.replace(key, newValue): replace a single element.map.replaceAll(BiFunction<K, V, V>): replace all elements according to logic.
BiFunction<K, V, V>: Takes two arguments (key, value) and returns the new value.
// Fichier : m5collections/src/main/java/map/MapRemoveReplaceDuplicates.java
package map;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
public class MapRemoveReplaceDuplicates {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(Map.of(
1, "Java", 2, "C#", 3, "JavaScript", 4, "Python", 5, "Go", 6, "Kotlin"));
map.remove(1);
System.out.println("Simple remove: " + map);
map.entrySet().removeIf(entry -> entry.getValue().endsWith("#"));
System.out.println("Removed by checking value: " + map);
map.entrySet().removeIf(entry -> entry.getKey().equals(3));
System.out.println("Removed by checking key: " + map);
map.replace(4, "Go");
System.out.println("Simple replace: " + map);
BiFunction<Integer, String, String> replace = (k, v) -> {
if ("Go".equals(v) || k == 6) return "Java";
return v;
};
map.replaceAll(replace);
System.out.println(map);
}
}
6.15 Checking the equality of two Maps (equals, hashCode)
With native types and records: map1.equals(map2) works directly.
With custom classes: If equals() and hashCode() are not overridden, Java compares references → false even for identical contents.
Two solutions:
- Overrider
equals()andhashCode()in the class. - Compare the values manually (via stream).
// Fichier : m5collections/src/main/java/map/MapCheckEquality.java
package map;
import java.util.Map;
public class MapCheckEquality {
public static void main(String[] args) {
// Avec types natifs — fonctionne directement
Map<Integer, String> mapOne = Map.of(1, "Java", 2, "C#", 3, "JavaScript");
Map<Integer, String> mapTwo = Map.of(1, "Java", 2, "C#", 3, "JavaScript");
System.out.println(mapOne.equals(mapTwo)); // true
// Avec records — fonctionne aussi
Map<Integer, LanguageRecord> mapLangRecordOne = Map.of(1, new LanguageRecord("Java"));
Map<Integer, LanguageRecord> mapLangRecordTwo = Map.of(1, new LanguageRecord("Java"));
System.out.println("With records: " + mapLangRecordOne.equals(mapLangRecordTwo)); // true
// Avec classe sans equals/hashCode — false
Map<Integer, Language> mapLangOne = Map.of(1, new Language("Java"));
Map<Integer, Language> mapLangTwo = Map.of(1, new Language("Java"));
System.out.println("Class without equals: " + mapLangOne.equals(mapLangTwo)); // false
// Comparaison par valeurs uniquement
System.out.println("By values only: " + areMapsEqualByValue(mapLangOne, mapLangTwo));
}
private static boolean areMapsEqualByValue(Map<Integer, Language> first, Map<Integer, Language> second) {
if (first.size() != second.size()) return false;
return first.entrySet().stream()
.allMatch(e -> e.getValue().getName().equals(second.get(e.getKey()).getName()));
}
static class Language {
String name;
public Language(String name) { this.name = name; }
public String getName() { return name; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return name.equals(((Language) o).name);
}
@Override public int hashCode() { return name.hashCode(); }
}
record LanguageRecord(String name) {}
}
6.16 Sort a Map (TreeMap, Stream)
Reminder of implementations:
HashMap: order not guaranteed.TreeMap: natural sorting of keys (numbers, strings).LinkedHashMap: insertion order.
Sort by key: Wrap in a TreeMap.
new TreeMap<>(unsortedMap)
Sort by value: Use streams.
Warning: If the keys are of a custom type that does not implement Comparable, TreeMap will throw a ClassCastException. Solutions: implement Comparable, or provide a Comparator to the TreeMap constructor.
// Fichier : m5collections/src/main/java/map/MapSort.java
package map;
import java.util.*;
import static java.util.stream.Collectors.toMap;
public class MapSort {
public static void main(String[] args) {
Map<Integer, String> mapOne = Map.of(20, "Java", 30, "C#", 10, "JavaScript", 40, "Python");
System.out.println(mapOne); // ordre non garanti (HashMap)
System.out.println("TreeMap: " + new TreeMap<>(mapOne)); // trié par clé
Map<Language, String> mapTwo = Map.of(new Language(), "Java", new Language(), "C#");
System.out.println("Sort by value stream: " + sortByValueStream(mapTwo, Comparator.naturalOrder()));
}
public static <K, V> Map<K, V> sortByValueStream(Map<K, V> map, Comparator<? super V> c) {
return map.entrySet().stream()
.sorted(Map.Entry.comparingByValue(c))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue,
(v1, v2) -> v1, LinkedHashMap::new));
}
static class Language {
@Override public String toString() { return "Language"; }
}
}
6.17 Merge Maps (putAll, flatMap, toMap)
Simple merge without duplicate keys: putAll() (the last value overwrites the previous ones in case of collision).
Merge with conflict resolution (duplicate keys): Use Stream.flatMap() + Collectors.toMap() with a merge function.
// Fichier : m5collections/src/main/java/map/MapMerge.java
package map;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MapMerge {
public static void main(String[] args) {
Map<Integer, String> mapOne = Map.of(1, "Java", 2, "C#", 3, "JavaScript");
Map<Integer, String> mapTwo = Map.of(3, "Go", 5, "Python", 6, "Kotlin");
// Fusion simple (putAll)
Map<Integer, String> mergedMap = new HashMap<>();
mergedMap.putAll(mapOne);
mergedMap.putAll(mapTwo); // "Go" écrase "JavaScript" à la clé 3
System.out.println("putAll: " + mergedMap);
// Fusion avec résolution de conflit (concaténation des valeurs)
var mergedMapTwo = Stream.of(mapOne, mapTwo)
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(v1, v2) -> v1 + " or " + v2));
System.out.println(mergedMapTwo); // 3 → "JavaScript or Go"
// Résolution de conflit : conserver la valeur maximale (Integer)
Map<String, Integer> mapThree = Map.of("Java", 1, "C#", 2, "JavaScript", 3);
Map<String, Integer> mapFour = Map.of("JavaScript", 4, "Python", 5, "Kotlin", 6);
var mergedMapThree = Stream.of(mapThree, mapFour)
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::max));
System.out.println(mergedMapThree); // JavaScript → 4 (max de 3 et 4)
}
}
6.18 Third-party libraries: Apache Collections and Google Guava
Apache Commons Collections 4 (commons-collections4):
CollectionUtils.intersection(listA, listB): intersection of the two collections.CollectionUtils.collate(listA, listB): merges and sorts without removing duplicates.- Many more utility methods.
Google Guava:
Lists.reverse(list): reverses a list.- Huge library, deserves a dedicated course.
// Fichier : m5collections/src/main/java/ApacheDemo.java
import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import java.util.Collection;
import java.util.List;
public class ApacheDemo {
public static void main(String[] args) {
Collection<String> intersection = CollectionUtils.intersection(
List.of("a", "b", "c"),
List.of("b", "c", "d", "e"));
System.out.println(intersection); // [b, c]
Collection<String> result = CollectionUtils.collate(
List.of("a", "b", "c"),
List.of("b", "c", "d", "e"));
System.out.println(result); // [a, b, b, c, c, d, e]
List<String> reversed = Lists.reverse(List.of("e", "z", "f"));
System.out.println(reversed); // [f, z, e]
}
}
7. Write succinct I/O code
Module 7 — 55m 25s
7.1 Introduction
This module only covers operations on the local file system: reading/writing files, creating directories, etc. It does not address network connections (FTP, HTTP, etc.).
Module plan:
- Overview of I/O packages.
- Path separators and current directory.
- Read and write files easily.
- Directory operations.
- Demo application.
7.2 IO vs. NIO vs. NIO2
Historical evolution:
| Package | Java version | Description |
|---|---|---|
java.io | From the beginning | Original package with File class. Limitations: no copy method, many methods return boolean instead of throwing exceptions. Verbose code to read/write. |
java.nio | Java 1.4 | New features (buffers, channels, etc.) — not a direct replacement for java.io. |
java.nio.file (NIO2) | Java 7 | Preferred. Replacement for java.io.File. More intuitive and complete API. Path interface and Files class at the core. |
Quote from the Oracle Certified Professional Java Developer Guide: “NIO2 is a replacement for the legacy java.io.File class. The goal is to provide a more intuitive and richer API for working with files and directories. The preferred approach for new applications is to use the NIO2 package.”
Date analogy: Like java.util.Date (legacy) replaced by java.time.LocalDateTime (modern), java.io.File is replaced by java.nio.file.Path.
7.3 Conversion between File and Path
Check for the existence of a file:
file.exists()— oldFilemethod.Files.exists(path)— NIO2 method, more robust (handles symbolic links).
Conversion File → Path: file.toPath().
// Fichier : m6io/src/main/java/files/FileToPathConversion.java
package files;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileToPathConversion {
static String filePath = "m6io/src/main/resources/modules.txt";
public static void main(String[] args) {
// Ancienne méthode (java.io)
File file = new File(filePath);
System.out.println(file.exists());
// Nouvelle méthode (NIO2)
Path path = Path.of(filePath);
System.out.println(Files.exists(path));
// Conversion File → Path
Path path2 = file.toPath();
System.out.println(Files.exists(path2));
}
}
7.4 Use the correct path separator
Simple rule: Use slash (/) for all operating systems.
Why? The slash (/) is recognized by Windows, Linux and macOS. No need for conditional branching or retrieving File.separator.
Alternatively: Path.of(varargs) accepts multiple arguments (path components) and automatically handles the separator.
// Fichier : m6io/src/main/java/files/FileSeparatorDemo.java
package files;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileSeparatorDemo {
static String filePath = "m6io/src/main/resources/modules.txt"; // slash
static String filePath2 = "m6io\\src\\main\\resources\\modules.txt"; // backslash
public static void main(String[] args) throws IOException {
System.out.println(Files.exists(Path.of(filePath))); // true (Windows et Linux)
System.out.println(Files.exists(Path.of(filePath2))); // true (Windows uniquement)
// Version portable via varargs
Path path = Path.of("m6io", "src", "main", "resources", "modules.txt");
}
}
7.5 Determine base directory
Why? Never use absolute paths in code (valid only on the development machine). Build relative paths from the base directory.
Two equivalent methods:
System.getProperty("user.dir") // propriété système
Path.of("").toAbsolutePath().toString() // via Path
Result: the root directory of the project (where Maven/Gradle executes the code).
// Fichier : m6io/src/main/java/files/GetTheCurrentDirectory.java
package files;
import java.nio.file.Path;
public class GetTheCurrentDirectory {
public static void main(String[] args) {
System.out.println(System.getProperty("user.dir"));
System.out.println(Path.of("").toAbsolutePath());
// Depuis là, construire : src/main/resources/..., src/test/resources/..., etc.
}
}
7.6 Check access to a file (isRegularFile, isReadable)
Before reading a file: Check that it exists, that it is readable and that it is indeed a regular file (not a directory, nor a symbolic link).
Files.isRegularFile(path): Checks that it is a regular file (non-symlink, non-directory). Returns false if the file does not exist.
Files.isReadable(path): Checks read permissions.
AccessDeniedException: Thrown if trying to read a directory as a file.
// Fichier : m6io/src/main/java/files/CheckAccessToFile.java
package files;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static java.nio.file.Files.*;
public class CheckAccessToFile {
static String filePath = "m6io/src/main/resources/modules.txt";
public static void main(String[] args) throws IOException {
Path file = Path.of(filePath);
if (isFileAccessible(file)) {
System.out.println(readString(file));
} else {
System.out.println("Do something else");
}
}
public static boolean isFileAccessible(Path file) {
return Files.isRegularFile(file) && Files.isReadable(file);
}
}
7.7 Read small text files (Files.readString)
For files of a few MB (not GB): Use Files.readString() — returns all content as a single String.
Alternative: Files.readAllLines() — returns a List<String> of lines.
Encoding: Both methods accept an optional Charset (eg: StandardCharsets.UTF_8).
Exception handling: These methods throw IOException (checked exception).
// Fichier : m6io/src/main/java/files/HowToReadSmallTextFile.java
package files;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class HowToReadSmallTextFile {
static String filePath = "m6io/src/main/resources/modules.txt";
public static void main(String[] args) throws IOException {
// Lire tout en une String
System.out.println(readSmallFile(filePath));
// Obtenir un Stream<String> de lignes depuis la String
readSmallFile(filePath).lines();
// Lire ligne par ligne dans une List<String>
System.out.println(readLineByLine(filePath));
readLineByLine(filePath).stream();
}
public static String readSmallFile(String pathToFile) throws IOException {
return Files.readString(Path.of(pathToFile));
}
public static List<String> readLineByLine(String pathToFile) throws IOException {
return Files.readAllLines(Path.of(pathToFile));
}
}
7.8 Read large text files (BufferedReader, Files.lines)
For large files (GB): Avoid loading everything into memory. Use BufferedReader or Files.lines().
BufferedReader provides a major performance gain (I/O buffering).
Two modern styles:
Style 1 — Files.newBufferedReader() + while loop:
try (BufferedReader br = Files.newBufferedReader(Path.of(path))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
Style 2 — Files.lines() + Streams API:
try (Stream<String> lines = Files.lines(Path.of(pathToFile))) {
return lines.filter(...).toList();
}
Important: With Files.lines(), use a try-with-resources to close the underlying stream and free the file resources.
// Fichier : m6io/src/main/java/files/HowToReadBigTextFile.java
package files;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
public class HowToReadBigTextFile {
static String filePath = "m6io/src/main/resources/modules.txt";
public static void main(String[] args) {
System.out.println(readWithBufferedReaderOldWay(filePath));
printWithBufferedReaderNewWay(filePath);
printWithStream(filePath);
}
// Style ancien (héritage) — à reconnaître
public static String readWithBufferedReaderOldWay(String path) {
BufferedReader objReader = null;
StringBuilder fileContent = new StringBuilder();
try {
String strCurrentLine;
objReader = new BufferedReader(new FileReader(path));
while ((strCurrentLine = objReader.readLine()) != null) {
fileContent.append(strCurrentLine).append(" ");
}
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
if (objReader != null) {
try { objReader.close(); } catch (IOException e) { throw new UncheckedIOException(e); }
}
}
return fileContent.toString();
}
// Style moderne — try-with-resources
public static void printWithBufferedReaderNewWay(String path) {
try (BufferedReader br = Files.newBufferedReader(Path.of(path))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
// Style Streams — le plus concis
public static List<String> printWithStream(String pathToFile) {
try (Stream<String> lines = Files.lines(Path.of(pathToFile))) {
return lines.toList();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
7.9 Read and write binary files
Reading/writing binary files is more complex and involves java.io packages with chained streams and while loops. This topic deserves a separate in-depth study.
7.10 Write to a file (Files.writeString, StandardOpenOption)
Files.writeString(path, content, options...) :
- Create the file if it does not exist.
- Default: overwrites existing content.
- Throws
FileAlreadyExistsExceptionwithStandardOpenOption.CREATE_NEW. - Appends to existing content with
StandardOpenOption.APPEND.
StandardOpenOption (enum): CREATE, CREATE_NEW, APPEND, TRUNCATE_EXISTING, WRITE, etc.
Hint — unique file names with timestamp:
String.format("file-%s.txt", Instant.now().toString().replace(":", "-"))
Empty a file without deleting it:
Files.writeString(Path.of(filePath), ""); // écrire une String vide
// Fichier : m6io/src/main/java/files/HowToWriteFile.java
package files;
import java.io.IOException;
import java.nio.file.*;
import java.time.Instant;
import java.util.List;
public class HowToWriteFile {
static String filePath = "m6io/src/main/resources/write_to_me.txt";
public static void main(String[] args) throws IOException {
// Écrire (crée si inexistant, écrase si existant)
Files.writeString(Path.of(filePath), "some str\n");
// Lève FileAlreadyExistsException si le fichier existe
// Files.writeString(Path.of(filePath), "another str\n", StandardOpenOption.CREATE_NEW);
// Ajouter à la fin
Files.writeString(Path.of(filePath), "some str 2\n", StandardOpenOption.APPEND);
// Fichier avec timestamp unique (ne jamais écraser)
Files.writeString(Path.of(String.format("m6io/src/main/resources/write_to_me-%s.txt",
Instant.now().toString().replace(":", "-"))), "some str\n");
// Vider le fichier
Files.writeString(Path.of(filePath), "");
// Bonne pratique : construire tout le contenu d'abord, puis écrire en une fois
List<String> strings = List.of("one", "two", "three");
StringBuilder sb = new StringBuilder();
for (String string : strings) {
sb.append(string).append(System.lineSeparator());
}
Files.writeString(Path.of(filePath), sb.toString(), StandardOpenOption.APPEND);
}
}
7.11 Copy a file (Files.copy)
Files.copy(source, destination, options...) :
- Default: throws
FileAlreadyExistsExceptionif the destination file exists. - To overwrite:
StandardCopyOption.REPLACE_EXISTING. - Other options:
COPY_ATTRIBUTES,ATOMIC_MOVE.
// Fichier : m6io/src/main/java/files/CopyFile.java
package files;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class CopyFile {
static String resources = "m6io/src/main/resources/";
static Path filePath = Path.of(resources + "modules.txt");
static Path filePath2 = Path.of(resources + "modules-copied.txt");
public static void main(String[] args) throws IOException {
// Copier en écrasant si existant
Files.copy(filePath, filePath2, StandardCopyOption.REPLACE_EXISTING);
}
}
Elegant handling of existing files (no exceptions):
if (!Files.exists(copyPath)) {
Files.copy(original, copyPath);
} else {
// Ajouter un timestamp au nom
Files.copy(original, copyPathWithTimestamp);
}
7.12 Move or rename a file (Files.move)
Files.move(source, destination, options...) :
- Move the file to another directory.
- If the destination directory is the same: rename the file.
- Target directory must exist.
- Throws
NoSuchFileExceptionif the target directory does not exist.
// Fichier : m6io/src/main/java/files/MoveFile.java
package files;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class MoveFile {
static String resources = "m6io/src/main/resources/";
public static void main(String[] args) throws IOException {
// Renommage (même répertoire, nom différent)
Files.move(Path.of(resources + "move_me.txt"),
Path.of(resources + "move_me-new.txt"), StandardCopyOption.REPLACE_EXISTING);
}
}
7.13 Delete a file (Files.delete)
Two methods:
Files.delete(path):void— throwsNoSuchFileExceptionif the file does not exist.Files.deleteIfExists(path): returnsboolean—trueif deleted,falseif non-existent.
Note on symbolic links: Both methods delete the symbolic link itself, not its target. This avoids unwanted cascading deletions.
// Fichier : m6io/src/main/java/files/DeleteFile.java
package files;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class DeleteFile {
static String dirPath = "m6io/src/main/resources/deletedir/";
public static void main(String[] args) throws IOException {
Path file = Path.of(dirPath + "delete_me.txt");
Files.delete(file); // void — lève exception si inexistant
System.out.println(Files.deleteIfExists(file)); // boolean — false si inexistant
}
}
7.14 Iterate over files in a directory (DirectoryStream)
Files.newDirectoryStream(path): Returns a DirectoryStream<Path> — an object iterable over the contents of the directory.
Features:
- Iterates only the first level of the directory (not recursive).
- Implement
Iterable→ usable in a for-each. - Must be closed (use try-with-resources).
Filtering: Version overloaded with pattern glob ("*.{log,txt}") or a DirectoryStream.Filter<Path>.
Files.isRegularFile(path): Use in the loop to exclude subdirectories and symbolic links.
// Fichier : m6io/src/main/java/files/FilterThroughFiles.java
package files;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class FilterThroughFiles {
public static void main(String[] args) throws IOException {
Path path = Path.of("iterateoverfiles");
// Itération simple
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) {
for (Path file : ds) {
System.out.println(file);
}
}
// Collecter le contenu des fichiers réguliers
var sb = new StringBuilder();
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) {
for (Path file : ds) {
if (Files.isRegularFile(file)) {
String content = Files.readString(file);
sb.append(content).append(System.lineSeparator());
}
}
}
System.out.println(sb);
// Filtrage par extension (glob)
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, "*.{csv}")) {
for (Path file : ds) {
if (Files.isRegularFile(file)) {
System.out.println(Files.readString(file));
}
}
}
// Filtre personnalisé (taille)
DirectoryStream.Filter<Path> sizeFilter = somePath -> {
long sizeLimit = 20; // bytes
return Files.size(somePath) > sizeLimit;
};
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, sizeFilter)) {
for (Path file : ds) {
if (Files.isRegularFile(file)) {
System.out.println(Files.readString(file));
}
}
}
// Filtre complexe combiné
DirectoryStream.Filter<Path> complexFilter = somePath -> {
boolean isFile = Files.isRegularFile(somePath);
boolean isCsv = somePath.getFileName().toString().endsWith("csv");
boolean isUnderOneMb = Files.size(somePath) < 1024 * 1024;
return isFile && isCsv && isUnderOneMb;
};
}
}
7.15 Browse file tree (walkFileTree, FileVisitor)
Files.walkFileTree(path, FileVisitor): Recursively visits all entries in a tree (implements the Visitor Pattern).
FileVisitor<Path> (interface):
preVisitDirectory(dir, attrs): before visiting a directory.visitFile(file, attrs): for each file — to implement.visitFileFailed(file, exc): in case of error during the visit.postVisitDirectory(dir, exc): after visiting a directory.
SimpleFileVisitor<Path>: Ready-to-use implementation (to be extended by overriding only the necessary methods).
FileVisitResult: Return enum — CONTINUE, TERMINATE, SKIP_SUBTREE, SKIP_SIBLINGS.
// Fichier : m6io/src/main/java/files/IterateOverFiles.java
package files;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
public class IterateOverFiles {
public static void main(String[] args) throws IOException {
List<Path> files = iterateWithVisitor(Path.of("iterateoverfiles"));
System.out.println(files);
for (Path file : files) {
System.out.println(Files.readString(file));
}
}
public static List<Path> iterateWithVisitor(Path dir) {
List<Path> files = new ArrayList<>();
FileVisitor<Path> allFiles = new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("File visited: " + file.toString());
files.add(file);
return FileVisitResult.CONTINUE;
}
};
try {
Files.walkFileTree(dir, allFiles);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return files;
}
}
7.16 Create a directory (createDirectory, createDirectories)
Files.createDirectory(path):
- Creates a single directory.
- Throw
FileAlreadyExistsExceptionif it already exists. - Throws
NoSuchFileExceptionif parent directory does not exist.
Files.createDirectories(path):
- Creates all necessary directories in path (recursive).
- Does not fail if directories already exist.
// Fichier : m6io/src/main/java/directories/CreateNewDirectory.java
package directories;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CreateNewDirectory {
public static void main(String[] args) throws IOException {
Path newDir = Path.of("newDir");
// Créer seulement s'il n'existe pas
if (!Files.exists(newDir)) {
Files.createDirectory(newDir);
}
// Créer plusieurs niveaux en une fois
Files.createDirectories(Path.of("topdir/subdir"));
}
}
7.17 Copy and delete a directory (Apache FileUtils)
Problem: Files.copy(sourceDir, destDir) only copies an empty directory. Files.delete(dir) throws DirectoryNotEmptyException on a non-empty directory.
Solution — Apache Commons IO (FileUtils):
FileUtils.copyDirectory(srcFile, destFile): Recursive copy with all subfiles.FileUtils.deleteDirectory(file): Complete recursive deletion.- Note: These methods expect
Fileobjects → usepath.toFile()to convert.
// Fichier : m6io/src/main/java/directories/CopyDeleteDirs.java
package directories;
import org.apache.commons.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CopyDeleteDirs {
public static void main(String[] args) throws IOException {
Path dir = Path.of("iterateoverfiles");
// NIO2 — ne copie que le répertoire vide (pas les sous-fichiers !)
Files.copy(dir, Path.of("copy-iterateoverfiles"));
// NIO2 — lève DirectoryNotEmptyException si le répertoire n'est pas vide
// Files.delete(dir);
// Apache Commons — copie et suppression récursive complète
FileUtils.copyDirectory(dir.toFile(), Path.of("copy-iterateoverfiles").toFile());
FileUtils.deleteDirectory(dir.toFile());
}
}
// Fichier : m6io/src/main/java/directories/CopyRenameDirectory.java
package directories;
import org.apache.commons.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CopyRenameDirectory {
static String dirPath = "copydir/";
public static void main(String[] args) throws IOException {
Files.copy(Path.of(dirPath), Path.of("dircopied")); // copie du répertoire vide
// FileUtils.deleteDirectory(Path.of(dirPath).toFile()); // suppression complète
}
}
7.18 Demo application: collect and copy log files with errors
Scenario: In a logs directory, identify files containing an [ERROR] line and copy them to an errors/ directory.
Steps:
Files.newDirectoryStream(dir, "*.{log}"): filter.logfiles.- For each file:
Files.readString(file)→ checkcontent.contains("[ERROR]"). - Collect the paths in a
List<Path>. - If list not empty: create the
errors/directory if non-existent, then copy each file.
// Fichier : m6io/src/main/java/demo/DemoApp.java
package demo;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
public class DemoApp {
public static void main(String[] args) throws IOException {
Path dir = Path.of("demoapp/logs");
List<Path> logsWithErrors = new ArrayList<>();
try (DirectoryStream<Path> files = Files.newDirectoryStream(dir, "*.{log}")) {
for (Path file : files) {
String content = Files.readString(file);
if (content.contains("[ERROR]")) {
logsWithErrors.add(file);
}
}
}
System.out.println(logsWithErrors);
Path logsWithErrorsDir = Path.of("demoapp/errors");
if (!logsWithErrors.isEmpty()) {
if (!Files.exists(logsWithErrorsDir)) {
Files.createDirectory(logsWithErrorsDir);
}
for (Path file : logsWithErrors) {
Files.copy(file, Path.of(String.format("demoapp/errors/%s", file.getFileName().toString())),
StandardCopyOption.REPLACE_EXISTING);
}
}
}
}
Refactored version (better separation of responsibilities):
// Fichier : m6io/src/main/java/demo/DemoAppRefactored.java
package demo;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
public class DemoAppRefactored {
public static void main(String[] args) throws IOException {
Path dir = Path.of("demoapp/logs");
List<Path> logsWithErrors;
try (DirectoryStream<Path> files = Files.newDirectoryStream(dir, "*.{log}")) {
logsWithErrors = getLogsWithErrors(files);
}
Path logsWithErrorsDir = Path.of("demoapp/errors");
if (logsWithErrors.isEmpty()) return; // early return
if (!Files.exists(logsWithErrorsDir)) {
Files.createDirectory(logsWithErrorsDir);
}
for (Path file : logsWithErrors) {
Files.copy(file, Path.of(String.format("demoapp/errors/%s", file.getFileName().toString())),
StandardCopyOption.REPLACE_EXISTING);
}
}
private static List<Path> getLogsWithErrors(DirectoryStream<Path> files) throws IOException {
List<Path> logsWithErrors = new ArrayList<>();
for (Path file : files) {
String content = Files.readString(file);
if (content.contains("[ERROR]")) {
logsWithErrors.add(file);
}
}
return logsWithErrors;
}
}
7.19 To go further
For in-depth expertise on I/O, NIO and NIO2 packages:
- Java Fundamentals: Input/Output by Jose Paumard (Java Champion) — covers IO, NIO and NIO2 in depth, with a module dedicated to reading/writing binary files.
- Other courses by author Andrejs Doronins: test automation, Java clean code, best practices, refactoring, design patterns.
8. Maven project structure
pom.xml main (multi-modules):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Java17-Playbook</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
<modules>
<module>m2strings</module>
<module>m3numbers</module>
<module>m4datetime</module>
<module>m5collections</module>
<module>m6io</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- Apache Commons Lang3 — StringUtils, RandomStringUtils, etc. -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- Apache Commons Collections4 — CollectionUtils, etc. -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<!-- Google Guava — Lists.reverse, etc. -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<!-- Apache Commons IO — FileUtils.copyDirectory, deleteDirectory -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- JUnit 5 — Tests unitaires -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Project directory structure:
Java17-Playbook-main/
├── pom.xml (POM multi-modules racine)
├── m2strings/
│ └── src/main/java/strings/
│ ├── RemoveSpacesDemo.java
│ ├── EmptyBlankDemo.java
│ ├── TransformStringDemo.java
│ ├── CompareStringsDemo.java
│ ├── IterateOverCharactersDemo.java
│ ├── ContainOnlyDigitDemo.java
│ ├── ContainsOnlySpecificCharDemo.java
│ ├── FindAndReplaceDemo.java
│ ├── StreamLinesDemo.java
│ ├── TokenizeDemo.java
│ ├── JoinStringsDemo.java
│ ├── BuildStringsInLoop.java
│ ├── MultilineStringsDemo.java
│ ├── LocalizationDemo.java
│ ├── RandomString.java
│ ├── ApacheDemo.java
│ └── ProgramDemo.java
├── m3numbers/
│ └── src/main/java/numbers/
│ ├── WrapperClassDemo.java
│ ├── fromstring/
│ │ ├── ConvertStringToNumber.java
│ │ └── CheckStringIsNumber.java
│ ├── formatting/
│ │ ├── FormattingDoubles.java
│ │ └── LocaleAndCompactFormatDoubles.java
│ ├── comparing/
│ │ ├── ComparingWholeNumbers.java
│ │ └── ComparingDecimalNumbers.java
│ ├── bigdecimal/
│ │ ├── BigNumbers.java
│ │ ├── RoundingModes.java
│ │ └── RoundingWithDecimalFormatAndBigDecimal.java
│ ├── random/
│ │ └── RandomValueMain.java
│ ├── ProgramDemo.java
│ └── ProgramDemoTwo.java
├── m4datetime/
│ └── src/main/java/
│ ├── overview/
│ │ └── ApiOverview.java
│ ├── dates/
│ │ ├── DifferenceBetweenDatesDemo.java
│ │ ├── CompareDatesDemo.java
│ │ ├── StartEndOfPeriodDemo.java
│ │ ├── HandlingRecurringEvents.java
│ │ ├── AddSubtractPeriods.java
│ │ ├── CumulativeDifferenceMain.java
│ │ ├── FormatDatesDemo.java
│ │ ├── MultipleFormatsDemo.java
│ │ └── ConvertOldDateDemo.java
│ ├── datetime/
│ │ ├── CurrentTimeMain.java
│ │ ├── GetAllZoneIds.java
│ │ ├── StartEndDay.java
│ │ └── CalculateArrivalTime.java
│ └── demoapp/
│ ├── DemoAppOne.java
│ └── DemoAppTwo.java
├── m5collections/
│ └── src/main/java/
│ ├── array/
│ │ ├── AddElementToArray.java
│ │ └── ConvertToFromArray.java
│ ├── list/
│ │ ├── CreateListAndAddElement.java
│ │ ├── CheckListIsEmpty.java
│ │ ├── RemoveElementsOfList.java
│ │ ├── RemoveDuplicates.java
│ │ ├── FindDuplicatesAndSimilarElements.java
│ │ ├── ReplaceElementInAList.java
│ │ ├── SortingSimple.java
│ │ └── SortingAdvanced.java
│ ├── map/
│ │ ├── MapOverview.java
│ │ ├── MapFindDuplicateValues.java
│ │ ├── MapRemoveReplaceDuplicates.java
│ │ ├── MapCheckEquality.java
│ │ ├── MapSort.java
│ │ └── MapMerge.java
│ └── ApacheDemo.java
└── m6io/
└── src/main/java/
├── files/
│ ├── FileToPathConversion.java
│ ├── FileSeparatorDemo.java
│ ├── GetTheCurrentDirectory.java
│ ├── CheckAccessToFile.java
│ ├── HowToReadSmallTextFile.java
│ ├── HowToReadBigTextFile.java
│ ├── HowToWriteFile.java
│ ├── CopyFile.java
│ ├── MoveFile.java
│ ├── DeleteFile.java
│ ├── FilterThroughFiles.java
│ └── IterateOverFiles.java
├── directories/
│ ├── CreateNewDirectory.java
│ ├── CopyRenameDirectory.java
│ └── CopyDeleteDirs.java
└── demo/
├── DemoApp.java
└── DemoAppRefactored.java
Search Terms
java · playbook · backend · architecture · full-stack · web · list · application · string · strings · dates · elements · check · convert · directory · map · numbers · text · time · apache · between · bigdecimal · compare · copy