Slide 1

Slide 1 text

Have you ever considered just not writing bugs? Tagir Valeev 1

Slide 2

Slide 2 text

2

Slide 3

Slide 3 text

About me ✓15 years of Java programming experience ✓7 years in JetBrains (IntelliJ IDEA Java team technical lead) ✓Contributed to FindBugs static analyzer (~2014) ✓OpenJDK committer ✓Java Champion ✓Wrote a book about Java mistakes 3

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

5 https://www.manning.com/books/100- java-mistakes-and-how-to-avoid-them https://www.amazon.com/dp/1633437965/

Slide 6

Slide 6 text

6 Promotion Code (45% off) 100 Java Mistakes (both e-book and paper) https://www.manning.com/books/100-java- mistakes-and-how-to-avoid-them valeevmu2

Slide 7

Slide 7 text

7 Bugs Huge and complex Tiny and local

Slide 8

Slide 8 text

8

Slide 9

Slide 9 text

How to avoid bugs? 9 ✓ Code style ✓ Idiomatic code ✓ Avoid repetitions ✓ Code review ✓ Pair programming ✓ Assertions ✓ AI! ✓ Static analysis ✓ Dynamic analysis (Pathfinder) ✓ Testing ✓ Unit tests ✓ + Coverage control (JaCoCo) ✓ + Mutation coverage control (Pitest) ✓ Smoke tests ✓ Functional tests ✓ Integration tests ✓ Property tests ✓ …

Slide 10

Slide 10 text

Bad news: there’s no silver bullet 10

Slide 11

Slide 11 text

11

Slide 12

Slide 12 text

Good news: Swiss cheese model 12 James Reason, 1990 https://en.wikipedia.org/wiki/Swiss_cheese_model

Slide 13

Slide 13 text

Initial capacity (StringBuilder) 13 static String indentString(String str, int indent) { int capacity = str.length() + indent < 0 ? 0 : indent; StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); return sb.toString(); }

Slide 14

Slide 14 text

Initial capacity 14 static String indentString(String str, int indent) { int capacity = str.length() + indent < 0 ? 0 : indent; StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); return sb.toString(); }

Slide 15

Slide 15 text

Initial capacity 15 static String indentString(String str, int indent) { int capacity = str.length() + indent < 0 ? 0 : indent; StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); return sb.toString(); } 1. indent >= 0 → capacity = indent >= 0 2. -str.length() <= indent < 0 → capacity = indent < 0 3. indent < -str.length() → capacity = 0

Slide 16

Slide 16 text

Let’s ask AI 16

Slide 17

Slide 17 text

17

Slide 18

Slide 18 text

18

Slide 19

Slide 19 text

19

Slide 20

Slide 20 text

What’s wrong here? 20 static String indentString(String str, int indent) { int capacity = str.length() + (indent < 0 ? 0 : indent); StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); return sb.toString(); }

Slide 21

Slide 21 text

What’s wrong here? 21 static String indentString(String str, int indent) { int capacity = str.length() + indent < 0 ? 0 : indent; StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); return sb.toString(); } ✓ Initial capacity is not easily testable! ✓ Manual algorithm implementation ✓ Action at a distance ✓ Premature optimization?

Slide 22

Slide 22 text

Asserts to the rescue? 22 ✓ Initial capacity is not easily testable! ✓ Manual algorithm implementation ✓ Action at a distance ✓ Premature optimization? static String indentString(String str, int indent) { int capacity = str.length() + indent < 0 ? 0 : indent; StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); assert capacity == sb.length(); return sb.toString(); }

Slide 23

Slide 23 text

How to fix? 23 static String indentString(String str, int indent) { int capacity = str.length() + Math.max(0, indent); StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); return sb.toString(); } ✓ Initial capacity is not easily testable! ✓ Manual algorithm implementation ✓ Action at a distance ✓ Premature optimization?

Slide 24

Slide 24 text

What’s wrong here? 24 static String indentStringAndLineBreak(String str, int indent) { int capacity = str.length() + Math.max(0, indent) + 1; StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); sb.append('\n'); return sb.toString(); } ✓ Initial capacity is not easily testable! ✓ Manual algorithm implementation ✓ Action at a distance ✓ Premature optimization?

Slide 25

Slide 25 text

How to fix? 25 static String indentString(String str, int indent) { if (indent <= 0) return str; int capacity = str.length() + indent; StringBuilder sb = new StringBuilder(capacity); for (int i = 0; i < indent; i++) { sb.append(' '); } sb.append(str); return sb.toString(); } ✓ Initial capacity is not easily testable! ✓ Manual algorithm implementation ✓ Action at a distance ✓ Premature optimization?

Slide 26

Slide 26 text

Or probably this way? 26 static String indentString(String str, int indent) { if (indent <= 0) return str; return " ".repeat(indent) + str; } ✓ Initial capacity is not easily testable! ✓ Manual algorithm implementation ✓ Action at a distance ✓ Premature optimization?

Slide 27

Slide 27 text

Initial capacity (ArrayList) 27 List trimAndAdd(List input, String newItem) { List result = new ArrayList<>(input.size() + newItem == null ? 0 : 1); for (String s : input) { result.add(s.trim()); } if (newItem != null) { result.add(newItem.trim()); } return result; }

Slide 28

Slide 28 text

Precedence again 28 List trimAndAdd(List input, String newItem) { List result = new ArrayList<>(input.size() + newItem == null ? 0 : 1); for (String s : input) { result.add(s.trim()); } if (newItem != null) { result.add(newItem.trim()); } return result; }

Slide 29

Slide 29 text

Static analysis cheese slice helps 29

Slide 30

Slide 30 text

Write tests! import java.util.Set; class Utils { static String makeUniqueId(String id, Set usedIds) { int i = 1; String uniqueId = id; while (usedIds.contains(uniqueId)) { uniqueId = id + "_" + i; } return uniqueId; } } 30

Slide 31

Slide 31 text

AI can do this! 31

Slide 32

Slide 32 text

public final class UtilsTest { @Test public void testMakeUniqueId() { String id = "Test"; Set usedIds = new HashSet<>(); String newId = Utils.makeUniqueId(id, usedIds); assertEquals(id, newId); } @Test public void testMakeUniqueIdWithPresentId() { String id = "Test"; Set usedIds = new HashSet<>(); usedIds.add(id); String newId = Utils.makeUniqueId(id, usedIds); assertNotEquals(id, newId); } } 32

Slide 33

Slide 33 text

Check coverage 33

Slide 34

Slide 34 text

Oops! @Test public void testMakeUniqueIdWithTwoConflicts() { String newId = Utils.makeUniqueId("Test", Set.of("Test", "Test_1")); assertEquals("Test_2", newId); } 34

Slide 35

Slide 35 text

Oops! @Test public void testMakeUniqueIdWithTwoConflicts() { String newId = Utils.makeUniqueId("Test", Set.of("Test", "Test_1")); assertEquals("Test_2", newId); } 35

Slide 36

Slide 36 text

@Test public void testMakeUniqueIdWithTwoConflicts() { String newId = Utils.makeUniqueId("Test", Set.of("Test", "Test_1")); assertEquals("Test_2", newId); } import java.util.Set; class Utils { static String makeUniqueId(String id, Set usedIds) { int i = 1; String uniqueId = id; while (usedIds.contains(uniqueId)) { uniqueId = id + "_" + i; } return uniqueId; } } 36

Slide 37

Slide 37 text

import java.util.Set; class Utils { static String makeUniqueId(String id, Set usedIds) { int i = 1; String uniqueId = id; while (usedIds.contains(uniqueId)) { uniqueId = id + "_" + i; } return uniqueId; } } Idempotent loop body 1. The condition is false initially. The loop is not executed at all. 2. The condition is true initially, but the body makes it false. The loop is executed only once. 3. The condition is true initially, and the body doesn’t change it. The loop is infinite, and the program hangs. 37

Slide 38

Slide 38 text

Static analysis helps here! 38

Slide 39

Slide 39 text

Rule: If you have a loop, your tests should test at least cases with 0, 1, and 2+ iterations. 39

Slide 40

Slide 40 text

Corner cases static void printInclusive(int from, int to) { for (int i = from; i <= to; i++) { System.out.println(i); } } 40

Slide 41

Slide 41 text

AI to the rescue? 41

Slide 42

Slide 42 text

AI to the rescue? static void printInclusive(int from, int to) { for (int i = from; i <= to; i++) { System.out.println(i); } } 42

Slide 43

Slide 43 text

Let’s protect ourselves static void printInclusive(int from, int to) { if (from > to) { throw new IllegalArgumentException("from > to"); } if (to - from < 0 || to - from > 1000) { throw new IllegalArgumentException("too many numbers to process"); } for (int i = from; i <= to; i++) { System.out.println(i); } } 43

Slide 44

Slide 44 text

Are there still mistakes? static void printInclusive(int from, int to) { if (from > to) { throw new IllegalArgumentException("from > to"); } if (to - from < 0 || to - from > 1000) { throw new IllegalArgumentException("too many numbers to process"); } for (int i = from; i <= to; i++) { System.out.println(i); } } 44

Slide 45

Slide 45 text

Hm… static void printInclusive(int from, int to) { if (from > to) { throw new IllegalArgumentException("from > to"); } if (to - from < 0 || to - from > 1000) { throw new IllegalArgumentException("too many numbers to process"); } for (int i = from; i <= to; i++) { System.out.println(i); } } Bullshit: printInclusive(-2_000_000_000, 2_000_000_000); More null-checks for the null-check god Doubtful but okaaaay OMG C’mon it’s just a sample Are you crazy? 45

Slide 46

Slide 46 text

Oops static void printInclusive(int from, int to) { if (from > to) { throw new IllegalArgumentException("from > to"); } if (to - from < 0 || to - from > 1000) { throw new IllegalArgumentException("too many numbers to process"); } for (int i = from; i <= to; i++) { System.out.println(i); } } public static void main(String[] args) { printInclusive(Integer.MAX_VALUE - 10, Integer.MAX_VALUE); } 46

Slide 47

Slide 47 text

Oops static void printInclusive(int from, int to) { if (from > to) { throw new IllegalArgumentException("from > to"); } if (to - from > 1000) { throw new IllegalArgumentException("too many numbers to process"); } for (int i = from; i <= to; i++) { System.out.println(i); } } public static void main(String[] args) { printInclusive(Integer.MAX_VALUE - 10, Integer.MAX_VALUE); } 47

Slide 48

Slide 48 text

How to fix it? static void printInclusive(int from, int to) { if (from > to) { throw new IllegalArgumentException("from > to"); } if (to - from < 0 || to - from > 1000) { throw new IllegalArgumentException("too many numbers to process"); } for (int i = from; i >= from && i <= to; i++) { System.out.println(i); } } public static void main(String[] args) { printInclusive(Integer.MAX_VALUE - 10, Integer.MAX_VALUE); } 48

Slide 49

Slide 49 text

Real life https://github.com/JetBrains/intellij-community/commit/b56a1fc16d446c92528994e0d333cbfa9ade21cd 49

Slide 50

Slide 50 text

Use Stream API! 50

Slide 51

Slide 51 text

Convert to Kotlin! (Ctrl+Alt+Shift+K) fun printInclusive(from: Int, to: Int) { require(from <= to) { "from > to" } require(to - from < 0 || to - from <= 1000) { "too many numbers to process" } for (i in from..to) { println(i) } } @JvmStatic fun main(args: Array) { printInclusive(Int.MAX_VALUE - 10, Int.MAX_VALUE) } Output: 2147483637 2147483638 2147483639 2147483640 2147483641 2147483642 2147483643 2147483644 2147483645 2147483646 2147483647 51

Slide 52

Slide 52 text

Rule: You should have a language construct or library method for every useful and repeating task Task: iterate over closed range of numbers IntStream.rangeClosed(from, to) for (i in from..to) { … } for (int i = from; i <= to; i++) { … } 52

Slide 53

Slide 53 text

Clamping values static void printProgress(int percent) { if (percent > 100) { percent = 100; } if (percent < 0) { percent = 0; } System.out.println("Progress: " + percent + "%"); } 53

Slide 54

Slide 54 text

Clamping values static void printProgress(int percent) { percent = percent < 0 ? 0 : percent > 100 ? 100 : percent; System.out.println("Progress: " + percent + "%"); } 54

Slide 55

Slide 55 text

Clamping values static void printProgress(int percent) { percent = Math.max(Math.min(percent, 0), 100); System.out.println("Progress: " + percent + "%"); } 55

Slide 56

Slide 56 text

Clamping values static void printProgress(int percent) { percent = Math.max(Math.min(percent, 0), 100); System.out.println("Progress: " + percent + "%"); } public static void main(String[] args) { printProgress(-10); printProgress(10); printProgress(50); printProgress(90); printProgress(130); } Output: Progress: 100% Progress: 100% Progress: 100% Progress: 100% Progress: 100% 56

Slide 57

Slide 57 text

Clamping values 57

Slide 58

Slide 58 text

AI to the rescue! 58

Slide 59

Slide 59 text

In the wild https://github.com/jenkinsci/jenkins/commit/e00f99251e0b 59

Slide 60

Slide 60 text

Real solution (Java 21) 60

Slide 61

Slide 61 text

Real solution (Java 21) static void printProgress(int percent) { percent = Math.clamp(percent, 0, 100); System.out.println("Progress: " + percent + "%"); } public static void main(String[] args) { printProgress(-10); printProgress(10); printProgress(50); printProgress(90); printProgress(130); } 61

Slide 62

Slide 62 text

Poor man template 62 private static final String TEMPLATE = "Hello USER_NAME!"; static void greetUser(String user) { String greeting = TEMPLATE.replaceAll("USER_NAME", user); System.out.println(greeting); }

Slide 63

Slide 63 text

Poor man template 63 private static final String TEMPLATE = "Hello USER_NAME!"; static void greetUser(String user) { String greeting = TEMPLATE.replaceAll("USER_NAME", user); System.out.println(greeting); } greetUser("John"); Hello John!

Slide 64

Slide 64 text

Poor man template 64 private static final String TEMPLATE = "Hello USER_NAME!"; static void greetUser(String user) { String greeting = TEMPLATE.replaceAll("USER_NAME", user); System.out.println(greeting); } greetUser("$1lly name"); Exception in thread "main" java.lang.IndexOutOfBoundsException: No group 1 at java.base/java.util.regex.Matcher.checkGroup(Matcher.java:1818) at java.base/java.util.regex.Matcher.start(Matcher.java:496) at java.base/java.util.regex.Matcher.appendExpandedReplacement(Matcher.java:1107) at java.base/java.util.regex.Matcher.appendReplacement(Matcher.java:1014) at java.base/java.util.regex.Matcher.replaceAll(Matcher.java:1200) at java.base/java.lang.String.replaceAll(String.java:3065) at com.example.Utils.greetUser(Utils.java:8) at com.example.Utils.main(Utils.java:13)

Slide 65

Slide 65 text

Convenience comes at a cost 65 String.replace: replaces all substrings String.replaceAll: replaces all regular expression matches public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement); } Rules: when designing your API, note that too many “convenient” methods may make things confusing. Avoid stringly-typed code.

Slide 66

Slide 66 text

Stringly-typed code 66 static int countPathComponents(String fileName) { String[] components = fileName.split(File.separator); return (int) Stream.of(components) .filter(Predicate.not(String::isEmpty)).count(); } countPathComponents("/etc/passwd") → 2

Slide 67

Slide 67 text

On Windows machine 67 static int countPathComponents(String fileName) { String[] components = fileName.split(File.separator); return (int) Stream.of(components) .filter(Predicate.not(String::isEmpty)).count(); } countPathComponents("C:\\tmp\\file.txt") Exception in thread "main" java.util.regex.PatternSyntaxException: Unescaped trailing backslash near index 1 \ at java.base/java.util.regex.Pattern.error(Pattern.java:2204) at java.base/java.util.regex.Pattern.compile(Pattern.java:1951) at java.base/java.util.regex.Pattern.(Pattern.java:1576) at java.base/java.util.regex.Pattern.compile(Pattern.java:1101) at java.base/java.lang.String.split(String.java:3352) at java.base/java.lang.String.split(String.java:3443) at com.example.Utils.countPathComponents(Utils.java:12) at com.example.Utils.main(Utils.java:21)

Slide 68

Slide 68 text

Fix? 68 static int countPathComponents(String fileName) { return (int) Pattern.compile(File.separator, Pattern.LITERAL) .splitAsStream(fileName) .filter(Predicate.not(String::isEmpty)).count(); } countPathComponents("C:\\tmp\\file.txt")

Slide 69

Slide 69 text

Idiomatic? 69 static int countPathComponents(String fileName) { return Path.of(fileName).getNameCount(); }

Slide 70

Slide 70 text

Idiomatic! 70 static int countPathComponents(String fileName) { return Path.of(fileName).getNameCount(); } Rule (again): use proper types for your entities, and strong typing will help you to avoid bugs

Slide 71

Slide 71 text

Conclusion ✓ Use static analysis ✓ Write unit tests ✓ Clear constructs for every idiom ✓ Avoid repetitions ✓ Consult AI when in doubt but don’t rely on it too much ✓ No premature optimizations ✓ Educate yourself, read books ☺ 71

Slide 72

Slide 72 text

Thank you 72

Slide 73

Slide 73 text

73 Promotion Code (45% off) 100 Java Mistakes (both e-book and paper) https://www.manning.com/books/100-java- mistakes-and-how-to-avoid-them valeevmu2