Slide 1

Slide 1 text

Why use Groovy in 2025? Dr Paul King, VP Apache Groovy & Distinguished Engineer, Object Computing Apache Groovy: Repo: Slides: Twitter/X | Mastodon | Bluesky: https://groovy.apache.org/ https://groovy-lang.org/ https://github.com/paulk-asert/groovy-today https://speakerdeck.com/paulk/groovy-today @ApacheGroovy | @[email protected] | @groovy.apache.org

Slide 2

Slide 2 text

Agenda ● Top 5 Groovy features not in Java ● Extension methods ● Operator overloading ● AST transforms ● Dynamic features ● Extensible type checking ● Top 5 Groovy improvements to Java ● Better OO features ● Better functional features ● Better language features ● Repl (groovysh) ● Java features earlier ● Case Study

Slide 3

Slide 3 text

Agenda ● Top 5 Groovy features not in Java ● Extension methods ● Operator overloading ● AST transforms ● Dynamic features ● Extensible type checking ● Top 5 Groovy improvements to Java ● Better OO features ● Better functional features ● Better language features ● Repl (groovysh) ● Java features earlier ● Case Study But first the TL;DR version

Slide 4

Slide 4 text

Concise Code … private static boolean isInteger(String s) { return s.matches("-?\\d+"); } private static int partitionPoint(List list, Predicate predicate) { for (int i = 0; i < list.size(); i++) { if (!predicate.test(list.get(i))) { return i; } } return list.size(); } } // 10 times slower import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.function.Predicate; import java.util.stream.Collectors; public class SortNumericStrings { private static final Map WORD_NUMBERS = Map.of( "ZERO", 0, "ONE", 1, "TWO", 2, /* ...add more... */ "TEN", 10 ); public static void main(String[] args) { List nums = Arrays.asList("222", "ZERO", "4", "33", "PI", "TWO"); nums.sort(Comparator.comparing(SortNumericStrings::isInteger)); int pp = partitionPoint(nums, s -> !isInteger(s)); nums.subList(0, pp).sort(Comparator.reverseOrder()); nums.subList(pp, nums.size()).sort(Comparator.comparingInt(Integer::parseInt)); if (!nums.equals(List.of("ZERO", "TWO", "PI", "4", "33", "222"))) { throw new AssertionError("Sort mismatch: " + nums); } var result = nums.stream().map(s -> { if (isInteger(s)) { return Integer.parseInt(s); } else if ("PI".equals(s)) { return java.lang.Math.PI; } else if ("E".equals(s)) { return java.lang.Math.E; } else if (WORD_NUMBERS.containsKey(s)) { return WORD_NUMBERS.get(s); } else { throw new IllegalArgumentException("Unknown literal: " + s); } }).toList(); List expected = List.of(0, 2, Math.PI, 4, 33, 222); if (!result.equals(expected)) { throw new AssertionError("Transformation mismatch: " + result); } } … var nums = ['222', 'ZERO', '4', '33', 'PI', 'TWO'] assert nums.toSorted() == ['222', '33', '4', 'PI', 'TWO', 'ZERO'] nums.sort(String::isInteger) var pp = nums.partitionPoint{ !it.integer } nums.sort(0.. it.toInteger() case ['PI', 'E'] -> Math."$it" default -> BigInteger."$it" } } == [0, 2, 3.141592653589793, 4, 33, 222]

Slide 5

Slide 5 text

Extension methods • 2000 extension methods to 150+ JDK classes enhance JDK functionality • 350 new methods added in Groovy 5 Better

Slide 6

Slide 6 text

AST Transformations // imports not shown public class Book { private String $to$string; private int $hash$code; private final List authors; private final String title; private final Date publicationDate; private static final java.util.Comparator this$TitleComparator; private static final java.util.Comparator this$PublicationDateComparator; public Book(List authors, String title, Date publicationDate) { if (authors == null) { this.authors = null; } else { if (authors instanceof Cloneable) { List authorsCopy = (List) ((ArrayList) authors).clone(); this.authors = (List) (authorsCopy instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Set ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Map ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof List ? DefaultGroovyMethods.asImmutable(authorsCopy) : DefaultGroovyMethods.asImmutable(authorsCopy)); } else { this.authors = (List) (authors instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Set ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Map ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof List ? DefaultGroovyMethods.asImmutable(authors) : DefaultGroovyMethods.asImmutable(authors)); } } this.title = title; if (publicationDate == null) { this.publicationDate = null; } else { this.publicationDate = (Date) publicationDate.clone(); } } public Book(Map args) { if ( args == null) { args = new HashMap(); } ImmutableASTTransformation.checkPropNames(this, args); if (args.containsKey("authors")) { if ( args.get("authors") == null) { this .authors = null; } else { if (args.get("authors") instanceof Cloneable) { List authorsCopy = (List) ((ArrayList) args.get("authors")).clone(); this.authors = (List) (authorsCopy instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Set ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Map ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof List ? DefaultGroovyMethods.asImmutable(authorsCopy) : DefaultGroovyMethods.asImmutable(authorsCopy)); } else { List authors = (List) args.get("authors"); this.authors = (List) (authors instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Set ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Map ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof List ? DefaultGroovyMethods.asImmutable(authors) : DefaultGroovyMethods.asImmutable(authors)); } } } else { this .authors = null; } if (args.containsKey("title")) {this .title = (String) args.get("title"); } else { this .title = null;} if (args.containsKey("publicationDate")) { if (args.get("publicationDate") == null) { this.publicationDate = null; } else { this.publicationDate = (Date) ((Date) args.get("publicationDate")).clone(); } } else {this.publicationDate = null; } } … public Book() { this (new HashMap()); } public int compareTo(Book other) { if (this == other) { return 0; } Integer value = 0 value = this .title <=> other .title if ( value != 0) { return value } value = this .publicationDate <=> other .publicationDate if ( value != 0) { return value } return 0 } public static Comparator comparatorByTitle() { return this$TitleComparator; } public static Comparator comparatorByPublicationDate() { return this$PublicationDateComparator; } public String toString() { StringBuilder _result = new StringBuilder(); boolean $toStringFirst= true; _result.append("Book("); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getAuthors())); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getTitle())); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getPublicationDate())); _result.append(")"); if ($to$string == null) { $to$string = _result.toString(); } return $to$string; } public int hashCode() { if ( $hash$code == 0) { int _result = HashCodeHelper.initHash(); if (!(this.getAuthors().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getAuthors()); } if (!(this.getTitle().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getTitle()); } if (!(this.getPublicationDate().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getPublicationDate()); } $hash$code = (int) _result; } return $hash$code; } public boolean canEqual(Object other) { return other instanceof Book; } … public boolean equals(Object other) { if ( other == null) { return false; } if (this == other) { return true; } if (!( other instanceof Book)) { return false; } Book otherTyped = (Book) other; if (!(otherTyped.canEqual( this ))) { return false; } if (!(this.getAuthors() == otherTyped.getAuthors())) { return false; } if (!(this.getTitle().equals(otherTyped.getTitle()))) { return false; } if (!(this.getPublicationDate().equals(otherTyped.getPublicationDate()))) { return false; } return true; } public final Book copyWith(Map map) { if (map == null || map.size() == 0) { return this; } Boolean dirty = false; HashMap construct = new HashMap(); if (map.containsKey("authors")) { Object newValue = map.get("authors"); Object oldValue = this.getAuthors(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("authors", oldValue); } else { construct.put("authors", this.getAuthors()); } if (map.containsKey("title")) { Object newValue = map.get("title"); Object oldValue = this.getTitle(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("title", oldValue); } else { construct.put("title", this.getTitle()); } if (map.containsKey("publicationDate")) { Object newValue = map.get("publicationDate"); Object oldValue = this.getPublicationDate(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("publicationDate", oldValue); } else { construct.put("publicationDate", this.getPublicationDate()); } return dirty == true ? new Book(construct) : this; } public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(authors); out.writeObject(title); out.writeObject(publicationDate); } public void readExternal(ObjectInput oin) throws IOException, ClassNotFoundException { authors = (List) oin.readObject(); title = (String) oin.readObject(); publicationDate = (Date) oin.readObject(); } … static { this$TitleComparator = new Book$TitleComparator(); this$PublicationDateComparator = new Book$PublicationDateComparator(); } public String getAuthors(int index) { return authors.get(index); } public List getAuthors() { return authors; } public final String getTitle() { return title; } public final Date getPublicationDate() { if (publicationDate == null) { return publicationDate; } else { return (Date) publicationDate.clone(); } } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } private static class Book$TitleComparator extends AbstractComparator { public Book$TitleComparator() { } public int compare(Book arg0, Book arg1) { if (arg0 == arg1) { return 0; } if (arg0 != null && arg1 == null) { return -1; } if (arg0 == null && arg1 != null) { return 1; } return arg0.title <=> arg1.title; } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } } private static class Book$PublicationDateComparator extends AbstractComparator { public Book$PublicationDateComparator() { } public int compare(Book arg0, Book arg1) { if ( arg0 == arg1 ) { return 0; } if ( arg0 != null && arg1 == null) { return -1; } if ( arg0 == null && arg1 != null) { return 1; } return arg0 .publicationDate <=> arg1 .publicationDate; } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } } } @Immutable(copyWith = true) @Sortable(excludes = 'authors') @AutoExternalize class Book { @IndexedProperty List authors String title Date publicationDate } 10 lines of Groovy or 600 lines of Java

Slide 7

Slide 7 text

Domain Specific Languages assert XII + IX == XXI assert [LVII + LVII, V * III, IV..(V+I)] == [ CXIV, XV, IV..VI] assert switch(L) { case L -> '50 exactly' case XLV.. 'just below 50' case ~/LI{1,3}/ -> 'just above 50' default -> 'not close to 50' } == '50 exactly’ assert [X, IX, IV, V, VI].sort() == [iv, v, vi, ix, x] assert MMMCMXCIX + I == MMMM show the square root of 64 muestra la raíz cuadrada de 64 まず 64 の 平方根 を 表示する 显示 64 的平方根

Slide 8

Slide 8 text

Nice Repl (groovysh)

Slide 9

Slide 9 text

Community: Praise in the last week

Slide 10

Slide 10 text

Community: Releases in the last week Plus: Grails Spring Security 7.0.0-RC2, Grails Quartz 4.0.0-RC2, Grails Redis 5.0.0-RC2, Grails GitHub Actions 1.0.0 geb $ git diff --shortstat v7.0 v8.0.0 802 files changed, 17869 insertions(+), 8757 deletions(-) groovy $ git diff --shortstat GROOVY_4_0_28 GROOVY_5_0_1 1296 files changed, 78192 insertions(+), 55746 deletions(-) grails-core $ git diff --shortstat v6.2.3 v7.0.0-RC2 9586 files changed, 669127 insertions(+), 711063 deletions(-)

Slide 11

Slide 11 text

Agenda ● Top 5 Groovy features not in Java ● Extension methods ● Operator overloading ● AST transforms ● Dynamic features ● Extensible type checking ● Top 5 Groovy improvements to Java ● Case Study

Slide 12

Slide 12 text

Extension methods Conceptually extend a class with new methods • Your class, Java class, 3rd-party library class Benefits • Increased developer productivity/reduced cognitive load • Simpler code • Smaller dependency footprint ▪ Security & maintenance benefits • Improved out-of-the-box experience (batteries included) • Extensible

Slide 13

Slide 13 text

Extension methods (capitalizing a String) @Grab('org.apache.commons:commons-lang3:3.18.0') import org.apache.commons.lang3.StringUtils assert StringUtils.capitalize('fe') == 'Fe' @Grab('org.springframework:spring-core:6.2.10') import org.springframework.util.StringUtils as SpringStringUtils assert SpringStringUtils.capitalize('fi') == 'Fi' import org.codehaus.groovy.runtime.StringGroovyMethods assert StringGroovyMethods.capitalize('fo') == 'Fo' assert 'fum'.capitalize() == 'Fum'

Slide 14

Slide 14 text

Extension methods @Grab('org.apache.commons:commons-lang3:3.18.0') import org.apache.commons.lang3.StringUtils assert StringUtils.capitalize('fe') == 'Fe' @Grab('org.springframework:spring-core:6.2.10') import org.springframework.util.StringUtils as SpringStringUtils assert SpringStringUtils.capitalize('fi') == 'Fi' import org.codehaus.groovy.runtime.StringGroovyMethods assert StringGroovyMethods.capitalize('fo') == 'Fo' assert 'fum'.capitalize() == 'Fum'

Slide 15

Slide 15 text

Extension methods @Grab('org.apache.commons:commons-lang3:3.18.0') import org.apache.commons.lang3.StringUtils assert StringUtils.capitalize('fe') == 'Fe' @Grab('org.springframework:spring-core:6.2.10') import org.springframework.util.StringUtils as SpringStringUtils assert SpringStringUtils.capitalize('fi') == 'Fi' import org.codehaus.groovy.runtime.StringGroovyMethods assert StringGroovyMethods.capitalize('fo') == 'Fo' assert 'fum'.capitalize() == 'Fum'

Slide 16

Slide 16 text

Extension methods @Grab('org.apache.commons:commons-lang3:3.18.0') import org.apache.commons.lang3.StringUtils assert StringUtils.capitalize('fe') == 'Fe' @Grab('org.springframework:spring-core:6.2.10') import org.springframework.util.StringUtils as SpringStringUtils assert SpringStringUtils.capitalize('fi') == 'Fi' import org.codehaus.groovy.runtime.StringGroovyMethods assert StringGroovyMethods.capitalize('fo') == 'Fo' assert 'fum'.capitalize() == 'Fum'

Slide 17

Slide 17 text

Extension methods: for java.lang.String:

Slide 18

Slide 18 text

Extension methods: 2000+ methods across ~150 classes: boolean[] byte[] char[] double double[] double[][] float float[] groovy.lang.Closure groovy.lang.GString groovy.lang.GroovyObject groovy.lang.ListWithDefault groovy.lang.MetaClass groovy.sql.GroovyResultSet int[] int[][] java.awt.Container java.io.BufferedReader java.io.BufferedWriter java.io.Closeable java.io.DataInputStream java.io.File java.io.InputStream java.io.ObjectInputStream java.io.ObjectOutputStream java.io.OutputStream java.io.PrintStream java.io.PrintWriter java.io.Reader java.io.Writer java.lang.Appendable java.lang.AutoCloseable java.lang.Boolean java.lang.Byte[] java.lang.CharSequence java.lang.Character java.lang.Class java.lang.ClassLoader java.lang.Comparable java.lang.Double java.lang.Enum java.lang.Float java.lang.Integer java.lang.Iterable java.lang.Long java.lang.Number java.lang.Object java.lang.Object[] java.lang.Process java.lang.Runtime java.lang.String java.lang.StringBuffer java.lang.StringBuilder java.lang.String[] java.lang.System java.lang.System$Logger java.lang.Thread java.lang.Throwable java.lang.reflect.AnnotatedElement java.math.BigDecimal java.math.BigInteger java.net.ServerSocket java.net.Socket java.net.URL java.nio.file.Path java.sql.Date java.sql.ResultSet java.sql.ResultSetMetaData java.sql.Timestamp java.time.DayOfWeek java.time.Duration java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.Month java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Period java.time.Year java.time.YearMonth java.time.ZoneId java.time.ZoneOffset java.time.ZonedDateTime java.time.chrono.ChronoPeriod java.time.temporal.Temporal java.time.temporal.TemporalAccessor java.time.temporal.TemporalAmount java.util.AbstractCollection java.util.AbstractMap java.util.BitSet java.util.Calendar java.util.Collection java.util.Date java.util.Deque java.util.Enumeration java.util.Iterator java.util.List java.util.Map java.util.Optional java.util.OptionalDouble java.util.OptionalInt java.util.OptionalLong java.util.ResourceBundle java.util.Set java.util.SortedMap java.util.SortedSet java.util.Spliterator java.util.TimeZone java.util.Timer java.util.concurrent.BlockingQueue java.util.concurrent.Future java.util.regex.Matcher java.util.regex.Pattern java.util.stream.BaseStream java.util.stream.Stream javax.script.ScriptEngine javax.script.ScriptEngineManager javax.swing.AbstractButton javax.swing.ButtonGroup javax.swing.DefaultComboBoxModel javax.swing.DefaultListModel javax.swing.JComboBox javax.swing.JMenu javax.swing.JMenuBar javax.swing.JPopupMenu javax.swing.JTabbedPane javax.swing.JToolBar javax.swing.ListModel javax.swing.MutableComboBoxModel javax.swing.table.DefaultTableModel javax.swing.table.TableColumnModel javax.swing.table.TableModel javax.swing.tree.DefaultMutableTreeNode javax.swing.tree.MutableTreeNode javax.swing.tree.TreeNode javax.swing.tree.TreePath long long[] long[][] org.codehaus.groovy.ast.ASTNode org.codehaus.groovy.control.SourceUnit org.codehaus.groovy.macro.matcher.ASTMatcher org.codehaus.groovy.macro.runtime.MacroContext org.codehaus.groovy.runtime.NullObject org.w3c.dom.Element org.w3c.dom.NodeList short[]

Slide 19

Slide 19 text

Primitive array extension methods How to find maximum and absolute maximum? int[] nums = {10, 20, 15, -30, 5};

Slide 20

Slide 20 text

Primitive array extension methods public class JavaStreamsMax { private static Comparator comparator = Comparator.comparingInt(Math::abs); public static int max(int[] nums) { return Arrays.stream(nums).max().getAsInt(); } public static int maxAbs(int[] nums) { return Arrays.stream(nums).boxed().max(comparator).get(); } } int[] nums = {10, 20, 15, -30, 5};

Slide 21

Slide 21 text

Primitive array extension methods Comparator maxAbs = Comparator.comparingInt(Math::abs) nums.intStream().max().getAsInt() nums.stream().max(maxAbs).get() public class JavaStreamsMax { private static Comparator comparator = Comparator.comparingInt(Math::abs); public static int max(int[] nums) { return Arrays.stream(nums).max().getAsInt(); } public static int maxAbs(int[] nums) { return Arrays.stream(nums).boxed().max(comparator).get(); } } int[] nums = {10, 20, 15, -30, 5};

Slide 22

Slide 22 text

IntComparator maxAbs = (i, j) -> i.abs() <=> j.abs() nums.max() nums.max(maxAbs) Primitive array extension methods Comparator maxAbs = Comparator.comparingInt(Math::abs) nums.intStream().max().getAsInt() nums.stream().max(maxAbs).get() public class JavaStreamsMax { private static Comparator comparator = Comparator.comparingInt(Math::abs); public static int max(int[] nums) { return Arrays.stream(nums).max().getAsInt(); } public static int maxAbs(int[] nums) { return Arrays.stream(nums).boxed().max(comparator).get(); } } int[] nums = {10, 20, 15, -30, 5};

Slide 23

Slide 23 text

IntComparator maxAbs = (i, j) -> i.abs() <=> j.abs() nums.max() nums.max(maxAbs) Comparator maxAbs = Comparator.comparingInt(Math::abs) nums.intStream().max().getAsInt() nums.stream().max(maxAbs).get() public class JavaStreamsMax { private static Comparator comparator = Comparator.comparingInt(Math::abs); public static int max(int[] nums) { return Arrays.stream(nums).max().getAsInt(); } public static int maxAbs(int[] nums) { return Arrays.stream(nums).boxed().max(comparator).get(); } } Better Primitive array extension methods int[] nums = {10, 20, 15, -30, 5};

Slide 24

Slide 24 text

● For dynamic and static modes ○ Conventions allow IDE discovery Extension methods: write your own class FileExtensionMethods { static int getWordCount(File self) { self.text.split(/\w+/).size() } } file.getWordCount() file.wordCount

Slide 25

Slide 25 text

Agenda ● Top 5 Groovy features not in Java ● Extension methods ● Operator overloading ● AST transforms ● Dynamic features ● Extensible type checking ● Top 5 Groovy improvements to Java ● Case Study

Slide 26

Slide 26 text

Operator Overloading Let common operators work with user-defined types • Your class, Java class, 3rd-party library class Benefits • Increased readability • Improved consistency • Expressiveness • Extensible

Slide 27

Slide 27 text

Operator Overloading Operator Method Operator Method + a.plus(b) a[b] a.getAt(b) - a.minus(b) a[b] = c a.putAt(b, c) * a.multiply(b) a in b b.isCase(a) / a.div(b) << a.leftShift(b) % a.mod(b) >> a.rightShift(b) a.remainder(b) >>> a.rightShiftUnsigned(b) ** a.power(b) ==> a.implies(b) == a.equals(b) <=> a.compareTo(b) | a.or(b) ++ a.next() & a.and(b) -- a.previous() ^ a.xor(b) +a a.positive() as a.asType(b) -a a.negative() a() a.call() ~a a.bitwiseNegate()

Slide 28

Slide 28 text

Operator Overloading assertEquals(m3, m1.multiply(m2.power(2))); assert m3 == m1 * m2 ** 2 BigInteger Matrices org.apache.commons:commons-math3:3.6.1 assertEquals(BigInteger.valueOf(21), BigInteger.valueOf(12).add(BigInteger.valueOf(9))); assert 21G == 12G + 9G

Slide 29

Slide 29 text

Operator Overloading: Case Study Sneak Peek @Sortable @Canonical class RomanNumeral { private fmt = new RomanNumeralFormat() final int d RomanNumeral(String s) { d = fmt.parse(s.toUpperCase()) } RomanNumeral(int d) { this.d = d } def plus(RomanNumeral other) { new RomanNumeral(d + other.d) } def multiply(RomanNumeral other) { new RomanNumeral(d * other.d) } def power(RomanNumeral other) { new RomanNumeral(d ** other.d) } String toString() { fmt.format(d) } RomanNumeral next() { new RomanNumeral(d+1) } } assert XII + IX == XXI

Slide 30

Slide 30 text

Operator Overloading OperatorRename in Groovy 5 @OperatorRename(plus='add', multiply='scalarMultiply') def testMatrixOperations() { double[][] d = [ [1d, 0d], [0d, 1d] ] var m = MatrixUtils.createRealMatrix(d) assert m.add(m) == m.scalarMultiply(2) // methods unchanged assert m + m == m * 2 // additional operator mappings }

Slide 31

Slide 31 text

Agenda ● Top 5 Groovy features not in Java ● Extension methods ● Operator overloading ● AST transforms ● Dynamic features ● Extensible type checking ● Top 5 Groovy improvements to Java ● Case Study

Slide 32

Slide 32 text

AST Transformations Let the compiler generate boiler-plate code • Annotation and global forms supported Benefits • Less boilerplate code to write, read ▪ Improved productivity ▪ Cheaper LLM costs due to reduced input/output tokens ▪ Better maintainability • Safer more declarative code ▪ Capture design patterns, performance enhancements, or tricky idioms • Extensible

Slide 33

Slide 33 text

AST Transformations // imports not shown public class Book { private String $to$string; private int $hash$code; private final List authors; private final String title; private final Date publicationDate; private static final java.util.Comparator this$TitleComparator; private static final java.util.Comparator this$PublicationDateComparator; public Book(List authors, String title, Date publicationDate) { if (authors == null) { this.authors = null; } else { if (authors instanceof Cloneable) { List authorsCopy = (List) ((ArrayList) authors).clone(); this.authors = (List) (authorsCopy instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Set ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Map ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof List ? DefaultGroovyMethods.asImmutable(authorsCopy) : DefaultGroovyMethods.asImmutable(authorsCopy)); } else { this.authors = (List) (authors instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Set ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Map ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof List ? DefaultGroovyMethods.asImmutable(authors) : DefaultGroovyMethods.asImmutable(authors)); } } this.title = title; if (publicationDate == null) { this.publicationDate = null; } else { this.publicationDate = (Date) publicationDate.clone(); } } public Book(Map args) { if ( args == null) { args = new HashMap(); } ImmutableASTTransformation.checkPropNames(this, args); if (args.containsKey("authors")) { if ( args.get("authors") == null) { this .authors = null; } else { if (args.get("authors") instanceof Cloneable) { List authorsCopy = (List) ((ArrayList) args.get("authors")).clone(); this.authors = (List) (authorsCopy instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Set ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Map ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof List ? DefaultGroovyMethods.asImmutable(authorsCopy) : DefaultGroovyMethods.asImmutable(authorsCopy)); } else { List authors = (List) args.get("authors"); this.authors = (List) (authors instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Set ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Map ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof List ? DefaultGroovyMethods.asImmutable(authors) : DefaultGroovyMethods.asImmutable(authors)); } } } else { this .authors = null; } if (args.containsKey("title")) {this .title = (String) args.get("title"); } else { this .title = null;} if (args.containsKey("publicationDate")) { if (args.get("publicationDate") == null) { this.publicationDate = null; } else { this.publicationDate = (Date) ((Date) args.get("publicationDate")).clone(); } } else {this.publicationDate = null; } } … public Book() { this (new HashMap()); } public int compareTo(Book other) { if (this == other) { return 0; } Integer value = 0 value = this .title <=> other .title if ( value != 0) { return value } value = this .publicationDate <=> other .publicationDate if ( value != 0) { return value } return 0 } public static Comparator comparatorByTitle() { return this$TitleComparator; } public static Comparator comparatorByPublicationDate() { return this$PublicationDateComparator; } public String toString() { StringBuilder _result = new StringBuilder(); boolean $toStringFirst= true; _result.append("Book("); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getAuthors())); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getTitle())); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getPublicationDate())); _result.append(")"); if ($to$string == null) { $to$string = _result.toString(); } return $to$string; } public int hashCode() { if ( $hash$code == 0) { int _result = HashCodeHelper.initHash(); if (!(this.getAuthors().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getAuthors()); } if (!(this.getTitle().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getTitle()); } if (!(this.getPublicationDate().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getPublicationDate()); } $hash$code = (int) _result; } return $hash$code; } public boolean canEqual(Object other) { return other instanceof Book; } … public boolean equals(Object other) { if ( other == null) { return false; } if (this == other) { return true; } if (!( other instanceof Book)) { return false; } Book otherTyped = (Book) other; if (!(otherTyped.canEqual( this ))) { return false; } if (!(this.getAuthors() == otherTyped.getAuthors())) { return false; } if (!(this.getTitle().equals(otherTyped.getTitle()))) { return false; } if (!(this.getPublicationDate().equals(otherTyped.getPublicationDate()))) { return false; } return true; } public final Book copyWith(Map map) { if (map == null || map.size() == 0) { return this; } Boolean dirty = false; HashMap construct = new HashMap(); if (map.containsKey("authors")) { Object newValue = map.get("authors"); Object oldValue = this.getAuthors(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("authors", oldValue); } else { construct.put("authors", this.getAuthors()); } if (map.containsKey("title")) { Object newValue = map.get("title"); Object oldValue = this.getTitle(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("title", oldValue); } else { construct.put("title", this.getTitle()); } if (map.containsKey("publicationDate")) { Object newValue = map.get("publicationDate"); Object oldValue = this.getPublicationDate(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("publicationDate", oldValue); } else { construct.put("publicationDate", this.getPublicationDate()); } return dirty == true ? new Book(construct) : this; } public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(authors); out.writeObject(title); out.writeObject(publicationDate); } public void readExternal(ObjectInput oin) throws IOException, ClassNotFoundException { authors = (List) oin.readObject(); title = (String) oin.readObject(); publicationDate = (Date) oin.readObject(); } … static { this$TitleComparator = new Book$TitleComparator(); this$PublicationDateComparator = new Book$PublicationDateComparator(); } public String getAuthors(int index) { return authors.get(index); } public List getAuthors() { return authors; } public final String getTitle() { return title; } public final Date getPublicationDate() { if (publicationDate == null) { return publicationDate; } else { return (Date) publicationDate.clone(); } } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } private static class Book$TitleComparator extends AbstractComparator { public Book$TitleComparator() { } public int compare(Book arg0, Book arg1) { if (arg0 == arg1) { return 0; } if (arg0 != null && arg1 == null) { return -1; } if (arg0 == null && arg1 != null) { return 1; } return arg0.title <=> arg1.title; } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } } private static class Book$PublicationDateComparator extends AbstractComparator { public Book$PublicationDateComparator() { } public int compare(Book arg0, Book arg1) { if ( arg0 == arg1 ) { return 0; } if ( arg0 != null && arg1 == null) { return -1; } if ( arg0 == null && arg1 != null) { return 1; } return arg0 .publicationDate <=> arg1 .publicationDate; } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } } } @Immutable(copyWith = true) @Sortable(excludes = 'authors') @AutoExternalize class Book { @IndexedProperty List authors String title Date publicationDate } 10 lines of Groovy or 600 lines of Java

Slide 34

Slide 34 text

AST Transformations: Groovy 2.4, 2.5, 2.5 (improved), 3.0, 4.0, 5.0 • 80 AST transforms @NonSealed @RecordBase @Sealed @PlatformLog @GQ @Final @RecordType @POJO @Pure @Contracted @Ensures @Invariant @Requires @ClassInvariant @ContractElement @Postcondition @Precondition @OperatorRename

Slide 35

Slide 35 text

AST Transforms: Case Study Sneak Peek @Sortable @Canonical class RomanNumeral { private fmt = new RomanNumeralFormat() final int d RomanNumeral(String s) { d = fmt.parse(s.toUpperCase()) } RomanNumeral(int d) { this.d = d } def plus(RomanNumeral other) { new RomanNumeral(d + other.d) } def multiply(RomanNumeral other) { new RomanNumeral(d * other.d) } def power(RomanNumeral other) { new RomanNumeral(d ** other.d) } String toString() { fmt.format(d) } RomanNumeral next() { new RomanNumeral(d+1) } }

Slide 36

Slide 36 text

Agenda ● Top 5 Groovy features not in Java ● Extension methods ● Operator overloading ● AST transforms ● Dynamic features ● Extensible type checking ● Top 5 Groovy improvements to Java ● Case Study

Slide 37

Slide 37 text

Dynamic features Runtime metaprogramming • Add methods and properties at runtime • Lifecycle hooks like propertyMissing and methodMissing Often used in combination with flexible syntax features • Dangling closure builder pattern • Command chains

Slide 38

Slide 38 text

Dangling closure builder pattern (MarkupBuilder) def writer = new StringWriter() def pom = new MarkupBuilder(writer) pom.project { modelVersion('4.0.0') groupId('org.apache.groovy') artifactId('groovy-examples') version('1.0-SNAPSHOT') } assert writer.toString() == '''\ 4.0.0 org.apache.groovy groovy-examples 1.0-SNAPSHOT '''

Slide 39

Slide 39 text

Dangling closure builder pattern (Gradle build) def writer = new StringWriter() def pom = new MarkupBuilder(writer) pom.project { modelVersion('4.0.0') groupId('org.apache.groovy') artifactId('groovy-examples') version('1.0-SNAPSHOT') } assert writer.toString() == '''\ 4.0.0 org.apache.groovy groovy-examples 1.0-SNAPSHOT ''' plugins { id 'com.github.ben-manes.versions' version '0.52.0' id 'groovy' } repositories { mavenCentral() } def groovyVersion = '5.0.0' dependencies { implementation "org.apache.groovy:groovy:$groovyVersion" implementation "org.apache.groovy:groovy-test:$groovyVersion" testImplementation ('org.spockframework:spock-core:2.3-groovy-4.0') { exclude group: 'org.codehaus.groovy' } }

Slide 40

Slide 40 text

Dangling closure builder pattern (Jenkins Pipeline) def writer = new StringWriter() def pom = new MarkupBuilder(writer) pom.project { modelVersion('4.0.0') groupId('org.apache.groovy') artifactId('groovy-examples') version('1.0-SNAPSHOT') } assert writer.toString() == '''\ 4.0.0 org.apache.groovy groovy-examples 1.0-SNAPSHOT ''' plugins { id 'com.github.ben-manes.versions' version '0.52.0' id 'groovy' } repositories { mavenCentral() } def groovyVersion = '5.0.0' dependencies { implementation "org.apache.groovy:groovy:$groovyVersion" implementation "org.apache.groovy:groovy-test:$groovyVersion" testImplementation ('org.spockframework:spock-core:2.3-groovy-4.0') { exclude group: 'org.codehaus.groovy' } } pipeline { agent any options { // Timeout counter starts AFTER agent is allocated timeout(time: 1, unit: 'SECONDS') } stages { stage('Example') { steps { echo 'Hello World' } } } }

Slide 41

Slide 41 text

Command chains // Square root command chain def the, root def show(t) { [square: { r -> [of: { n -> println Math.sqrt(n) }] }] } show(the).square(root).of(64)

Slide 42

Slide 42 text

Command chains // Square root command chain def the, root def show(t) { [square: { r -> [of: { n -> println Math.sqrt(n) }] }] } show the square root of 64

Slide 43

Slide 43 text

Dynamic features: Supporting DSLs

Slide 44

Slide 44 text

Dynamic features: Supporting DSLs

Slide 45

Slide 45 text

Dynamic features: Case Study Sneak Peek def propertyMissing(String p) { new RomanNumeral(p) } assert XII + IX == XXI

Slide 46

Slide 46 text

Agenda ● Top 5 Groovy features not in Java ● Extension methods ● Operator overloading ● AST transforms ● Dynamic features ● Extensible type checking ● Top 5 Groovy improvements to Java ● Case Study

Slide 47

Slide 47 text

Extensible type checking • Groovy has static and dynamic modes • Static mode has Java-like type checking but is extensible • You can strengthen or weaken checking ▪ Strengthen when you want stronger-than-Java checking ▪ Selectively weaken for code with some small dynamic parts

Slide 48

Slide 48 text

Extensible type checker: write your own checkers > javac ValueOf.java > java ValueOf Exception in thread "main" java.lang.NumberFormatException: For input string: "five" void main() { int version = 20 + Integer.valueOf("five"); IO.println("Hello from Java " + version + "!"); }

Slide 49

Slide 49 text

Extensible type checker: write your own checkers > groovyc ValueOf.groovy [Static type checking] - Not a valid Integer: five @ line 6, column 35. int version = Integer.valueOf('five') ^ @TypeChecked(extensions = 'ConstantIntegerChecker.groovy') def main() { int version = Integer.valueOf('five') println "Hello from Groovy $version!" }

Slide 50

Slide 50 text

Extensible type checker: built-in regex checker import groovy.transform.TypeChecked @TypeChecked(extensions = 'groovy.typecheckers.RegexChecker') def whenIs2020Over() { def newYearsEve = '2020-12-31' def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/ } def newYearsEve = '2020-12-31' def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/ // PatternSyntaxException

Slide 51

Slide 51 text

Extensible type checker: built-in format string checker

Slide 52

Slide 52 text

Extensible type checker: Case Study Sneak Peek unresolvedVariable { VariableExpression ve -> try { new RomanNumeral(ve.name) storeType(ve, classNodeFor(RomanNumeral)) } catch(ParseException unused) { addStaticTypeError("Not a valid roman numeral: $ve.name", ve) } handled = true } [Static type checking] - Not a valid roman numeral: MMMM @ line 6, column 25. assert MMMCMXCIX + I == MMMM ^

Slide 53

Slide 53 text

Agenda ● Top 5 Groovy features not in Java ● Top 5 Groovy improvements to Java ● Better OO features ● Better functional features ● Better language features ● Repl (groovysh) ● Java features earlier ● Case Study

Slide 54

Slide 54 text

Better OO features • Traits • Records

Slide 55

Slide 55 text

Traits ● Can be like Java default interface methods import java.util.Collections; import java.util.List; public interface RotatableList extends List { default void rotate(int distance) { Collections.rotate(this, distance); } } import java.util.ArrayList; import java.util.List; public class RotatableListImpl extends ArrayList implements RotatableList { public RotatableListImpl() { super(List.of("p", "i", "n", "s")); } } public class RotateMain { public static void main(String[] args) { var myList = new RotatableListImpl(); myList.rotate(1); System.out.println(myList); } } [s, p, i, n]

Slide 56

Slide 56 text

● Can be like Java default interface methods Traits trait RotatableList implements List { void rotate(int distance) { Collections.rotate(this, distance) } } class RotatableListImpl extends ArrayList implements RotatableList { RotatableListImpl() { super(['p', 'i', 'n', 's']) } } var myList = new RotatableListImpl() myList.rotate(1) assert myList == ['s', 'p', 'i', 'n']

Slide 57

Slide 57 text

Traits ● But traits are more ambitious, supporting: ○ Sharing state information (stateful traits) ○ More flexible selection of functionality ■ Sharable behavior not just default behavior ■ An OO feature not just primarily about API evolution ○ Traits at runtime (dynamic traits) ○ Mixin/Stackable traits pattern (stackable traits)

Slide 58

Slide 58 text

Traits: Stateful Traits trait Notable { String note } trait Timestamped { Date created = new Date() } class MyList extends ArrayList implements Notable, Timestamped { } def shopping = new MyList(note: 'groceries for weekend') shopping += [' ', ' ', ' '] assert shopping.size() == 3 assert shopping.note == 'groceries for weekend' assert shopping.created.format('EEEE').endsWith('y')

Slide 59

Slide 59 text

Traits: Dynamic Traits trait Starable { String starify() { this.replaceAll('o', ' ') } } def groovy = 'Groovy' as Starable assert groovy.starify() == 'Gr vy'

Slide 60

Slide 60 text

Traits: More flexible behavior sharing • Groovy normalizes negative index values when using the index notation or getAt method, but not the get method • What if you also wanted this for the get method? def nums = [1, 2, 3] assert nums[-1] == 3 assert nums.getAt(-1) == 3 shouldFail(IndexOutOfBoundsException) { nums.get(-1) }

Slide 61

Slide 61 text

Traits: More flexible behavior sharing • Index normalization for the get method trait NormalizedGet implements List { E get(int index) { if (index < 0) index += size() super.get(index) } } class MyList extends ArrayList implements NormalizedGet {} nums = [1, 2, 3] as MyList assert nums.get(-1) == 3

Slide 62

Slide 62 text

Traits: Stackable Traits Pattern interface Handler { String handle(String message) } trait UpperHandler implements Handler { String handle(String message) { super.handle(message.toUpperCase()) } } trait ReverseHandler implements Handler { String handle(String message) { super.handle(message.reverse()) } } trait StarHandler implements Handler { String handle(String message) { message.replaceAll('O', ' ') } } class MyHandler implements StarHandler, ReverseHandler, UpperHandler {} assert new MyHandler().handle('yvoorG') == 'GR VY'

Slide 63

Slide 63 text

Traits: compared to interface methods Default Method in Interface Trait Comments Supported by Provide default behavior 🗹 🗹 Supports interface evolution Overridable behavior 🗷 🗹 Flexible behavior sharing Stateful behavior 🗷 🗹 Dynamic behavior 🗷 🗹 Stackable trait pattern 🗷 🗹 Static Method in Interface Trait Comments Utility methods 🗹 🗹

Slide 64

Slide 64 text

Classes and records (Java) public class Point { private int x; private int y; public int x() { return this.x; } public int y() { return this.y; } public String toString() { /* ... */ } public int hashCode() { /* ... */ } public boolean equals(Object other) { /* ... */ } } public record Point(int x, int y) { }

Slide 65

Slide 65 text

Classes and records (Groovy) record Point(int x, int y) { } @RecordType class Point { int x int y } class Point { private int x private int y int x() { this.x } int y() { this.y } String toString() { /* ... */ } int hashCode() { /* ... */ } boolean equals(Object other) { /* ... */ } }

Slide 66

Slide 66 text

Classes and records (Groovy) class Point { private int x private int y int x() { this.x } int y() { this.y } String toString() { /* ... */ } int hashCode() { /* ... */ } boolean equals(Object other) { /* ... */ } } record Point(int x, int y) { } @RecordBase @ToString @EqualsAndHashCode @RecordOptions @TupleConstructor @PropertyOptions @KnownImmutable class Point { int x int y }

Slide 67

Slide 67 text

AST Transformations: @Immutable meta-annotation @Immutable class Point { int x, y } @ToString(includeSuperProperties = true, cache = true) @EqualsAndHashCode(cache = true) @ImmutableBase @ImmutableOptions @PropertyOptions(propertyHandler = ImmutablePropertyHandler) @TupleConstructor(defaults = false) @MapConstructor(noArg = true, includeSuperProperties = true, includeFields = true) @KnownImmutable class Point { int x, y }

Slide 68

Slide 68 text

Declarative Record Customization def a = new Agenda(topics: ['Sealed', 'Records']) assert a.topics().size() == 2 assert a.toString() == 'Agenda[topics=[Sealed, Records]]' a.topics().clear() a.topics() << 'Switch Expressions' assert a.topics().size() == 1 record Agenda(List topics) { }

Slide 69

Slide 69 text

Declarative Record Customization def a = new Agenda(topics: ['Sealed', 'Records']) assert a.topics().size() == 2 shouldFail(UnsupportedOperationException) { a.topics().clear() } assert a.toString() == 'Agenda([Sealed, Records])' @ToString @PropertyOptions(propertyHandler = ImmutablePropertyHandler) record Agenda(List topics) { }

Slide 70

Slide 70 text

Using records with AST Transforms: @Memoized @Builder record Point(int x, int y, String color) { @Memoized String description() { "${color.toUpperCase()} point at ($x,$y)" } } record Developer(Integer id, String first, String last, String email, List skills) { @Builder Developer(Integer id, String full, String email, List skills) { this(id, full.split(' ')[0], full.split(' ')[1], email, skills) } }

Slide 71

Slide 71 text

Using records with AST Transforms: @Requires @Sortable @Requires({ color && !color.blank }) record Point(int x, int y, String color) { } @Sortable record Point(int x, int y, String color) { } var points = [ new Point(0, 100, 'red'), new Point(10, 10, 'blue'), new Point(100, 0, 'green'), ] println points.toSorted(Point.comparatorByX()) println points.toSorted(Point.comparatorByY()) println points.toSorted(Point.comparatorByColor())

Slide 72

Slide 72 text

record Quadratic(double a, double b = 0, double c = 0) { @Newify(Complex) List solve() { var discriminant = Complex(b * b - 4 * a * c) findRoots(Complex(-b), discriminant, Complex(2 * a)) } @OperatorRename(div = 'divide', plus = 'add', minus = 'subtract') static List findRoots(Complex negB, Complex discriminant, Complex twoA) { var sqrtDiscriminant = discriminant.sqrt() var root1 = (negB + sqrtDiscriminant) / twoA var root2 = (negB - sqrtDiscriminant) / twoA [root1, root2] } } Using records with AST Transforms: @Newify @OperatorRename • Representing a quadratic equation: ax2 + bx + c org.apache.commons:commons-numbers-core:1.1

Slide 73

Slide 73 text

assert [ new Quadratic(2.0, -4.0, 2.0), new Quadratic(2.0, -4.0), new Quadratic(1.0), new Quadratic(a:2.0, b:-5.0, c:-3.0) ]*.solve()*.toSet()*.toString() == [ '[(1.0, 0.0)]', '[(2.0, 0.0), (0.0, 0.0)]', '[(0.0, 0.0)]', '[(3.0, 0.0), (-0.5, 0.0)]' ] Using records with other features: Named/Default params Default parameters Named parameters

Slide 74

Slide 74 text

Records: compared to Java Java Record Groovy Emulated Record Groovy Native Record JDK version 16+ 8+ 16+ Serialization Record spec Traditional Record spec Recognized by Java, Groovy Groovy Java, Groovy Standard features • accessors • tuple constructor • toString, equals, hashCode 🗹 🗹 🗹 Optional enhancements 🗷 toMap, toList, size, getAt, components, copyWith, named-arg constructor, optional args Customisable via coding 🗹 🗹 🗹 Customisable via AST transforms (declarative) 🗷 🗹 🗹

Slide 75

Slide 75 text

Agenda ● Top 5 Groovy features not in Java ● Top 5 Groovy improvements to Java ● Better OO features ● Better functional features ● Better language features ● Repl (groovysh) ● Java features earlier ● Case Study

Slide 76

Slide 76 text

Better functional features • Tail recursion • Memoization

Slide 77

Slide 77 text

Closures • Somewhat like Lambdas • but let’s explore some recursion examples import java.math.BigInteger; import java.util.function.UnaryOperator; public class FactLambda { static UnaryOperator factorial; public static void main(String[] args) { factorial = n -> n.equals(BigInteger.ZERO) ? BigInteger.ONE : n.multiply(factorial.apply(n.subtract(BigInteger.ONE))); System.out.println(factorial.apply(BigInteger.valueOf(5))); // 120 System.out.println(factorial.apply(BigInteger.valueOf(8000))); // Boom! } } StackOverflowError

Slide 78

Slide 78 text

Closures: Naïve algorithm def factorial factorial = { it <= 1 ? 1G : it * factorial(it - 1) } println factorial(5) // 120 println factorial(8000) // StackOverFlow StackOverflowError factorial(5) 5 * factorial(4) 5 * (4 * factorial(3)) 5 * (4 * (3 * factorial(2))) 5 * (4 * (3 * (2 * factorial(1)))) 5 * (4 * (3 * (2 * 1))) 5 * (4 * (3 * 2)) 5 * (4 * 6) 5 * 24 120

Slide 79

Slide 79 text

Closures: Tail recursive algorithm def factorial factorial = { n, acc = 1G -> n <= 1 ? acc : factorial(n - 1, n * acc) } println factorial(5) // 120 println factorial(8000) // StackOverFlow StackOverflowError factorial(5, 1) factorial(4, 5) factorial(3, 20) factorial(2, 60) factorial(1, 120) 120

Slide 80

Slide 80 text

Closures: Tail recursive algorithm def factorial factorial = { n, acc = 1G -> n <= 1 ? acc : factorial(n - 1, n * acc) } println factorial(5) // 120 println factorial(8000) // StackOverFlow

Slide 81

Slide 81 text

Closures: Tail recursive with trampoline def factorial factorial = { n, acc = 1G -> n <= 1 ? acc : factorial.trampoline(n - 1, n * acc) }.trampoline() println factorial(5) println factorial(8000) println factorial(100_000) 120 5184…<27750 more digits> 28242…<456570 more digits>

Slide 82

Slide 82 text

Closures: Tail recursive with trampoline def factorial factorial = { n, acc = 1G -> n <= 1 ? acc : factorial.trampoline(n - 1, n * acc) }.trampoline() println factorial(5) println factorial(8000) println factorial(100_000) 120 5184…<27750 more digits> 28242…<456570 more digits> See also @TailRecursive

Slide 83

Slide 83 text

Closures: Memoize import java.math.BigInteger; import java.util.function.UnaryOperator; public class FiboLambda { static UnaryOperator fibI; static UnaryOperator fibL; static UnaryOperator fibBI; public static void main(String[] args) { fibI = n -> n <= 1 ? n : fibI.apply(n - 1) + fibI.apply(n - 2); fibL = n -> n <= 1 ? n : fibL.apply(n - 1) + fibL.apply(n - 2); fibBI = n -> n.compareTo(BigInteger.ONE) <= 0 ? N : fibBI.apply(n.subtract(BigInteger.ONE)).add(fibBI.apply(n.subtract(BigInteger.TWO))); var start = System.currentTimeMillis(); System.out.println(fibI.apply(10)); // 55 System.out.println(fibL.apply(50L)); // 12586269025 System.out.println(fibBI.apply(BigInteger.valueOf(100))); // 354224848179261915075 var end = System.currentTimeMillis(); var years = (end - start) / 1000 / 60 / 60.0 / 24 / 365.25; System.out.println("Completed in " + years + " years"); } }

Slide 84

Slide 84 text

Closures: Memoize import java.math.BigInteger; import java.util.function.UnaryOperator; public class FiboLambda { static UnaryOperator fibI; static UnaryOperator fibL; static UnaryOperator fibBI; public static void main(String[] args) { fibI = n -> n <= 1 ? n : fibI.apply(n - 1) + fibI.apply(n - 2); fibL = n -> n <= 1 ? n : fibL.apply(n - 1) + fibL.apply(n - 2); fibBI = n -> n.compareTo(BigInteger.ONE) <= 0 ? N : fibBI.apply(n.subtract(BigInteger.ONE)).add(fibBI.apply(n.subtract(BigInteger.TWO))); var start = System.currentTimeMillis(); System.out.println(fibI.apply(10)); // 55 System.out.println(fibL.apply(50L)); // 12586269025 System.out.println(fibBI.apply(BigInteger.valueOf(100))); // 354224848179261915075 var end = System.currentTimeMillis(); var years = (end - start) / 1000 / 60 / 60.0 / 24 / 365.25; System.out.println("Completed in " + years + " years"); } } 55 12586269025 354224848179261915075 Completed in 6.4E9 years* * Estimated Fibonacci time complexity has O(2n) as the upper bound

Slide 85

Slide 85 text

Closures: Memoize var fib fib = { n -> n <= 1 ? n : fib(n - 1) + fib(n - 2) } var start = System.currentTimeMillis() assert fib(10) == 55 assert fib(50L) == 12586269025L assert fib(100G) == 354224848179261915075G println "Completed in ${System.currentTimeMillis() - start} ms"

Slide 86

Slide 86 text

Closures: Memoize var fib fib = { n -> n <= 1 ? n : fib(n - 1) + fib(n - 2) }.memoize() var start = System.currentTimeMillis() assert fib(10) == 55 assert fib(50L) == 12586269025L assert fib(100G) == 354224848179261915075G println "Completed in ${System.currentTimeMillis() - start} ms"

Slide 87

Slide 87 text

Closures: Memoize See also @Memoized var fib fib = { n -> n <= 1 ? n : fib(n - 1) + fib(n - 2) }.memoize() var start = System.currentTimeMillis() assert fib(10) == 55 assert fib(50L) == 12586269025L assert fib(100G) == 354224848179261915075G println "Completed in ${System.currentTimeMillis() - start} ms" Completed in 117 ms

Slide 88

Slide 88 text

Agenda ● Top 5 Groovy features not in Java ● Top 5 Groovy improvements to Java ● Better OO features ● Better functional features ● Better language features ● Repl (groovysh) ● Java features earlier ● Case Study

Slide 89

Slide 89 text

Better language features • Switch Expressions

Slide 90

Slide 90 text

Language features: switch expressions class CustomIsCase { boolean isCase(subject) { subject > 20 } } assert switch(10) { case 0 -> false case '11' -> false case null -> false case 0..9 -> false case 1, 2 -> false case [9, 11, 13] -> false case Float -> false case { it % 3 == 0 } -> false case new CustomIsCase() -> false case ~/\d\d/ -> true default -> false }

Slide 91

Slide 91 text

● Duck & flow typing support styles which otherwise need special support when using static typing String formatted = switch (o) { case Integer i when i > 10 -> String.format("a large Integer %d", i); case Integer i -> String.format("a small Integer %d", i); case Long l -> String.format("a Long %d", l); default -> o.toString(); }; String formatted = switch (o) { case { o instanceof Integer && o > 10 } -> "a large Integer $o" case Integer -> "a small Integer $o" case Long -> "a Long $o" default -> o.toString() } JDK8 JDK21 with preview features enabled Language features: switch expressions

Slide 92

Slide 92 text

Language features: switch expressions (summary) Java Groovy JDK versions 14+ 8+ “ -> ” syntax (switch rule) 🗹 🗹 “ : … yield” syntax 🗹 🗹 Supported case selector expressions Constant • Boolean, Number, String Enum constant Constant • Boolean, Number, String, null Enum constant List expression Range expression Closure Regex Pattern Class expression Enhanced switch (JDK21+) Type pattern Guard/when Null Class or Closure Closure already supported Extensible via isCase 🗷 🗹

Slide 93

Slide 93 text

Agenda ● Top 5 Groovy features not in Java ● Top 5 Groovy improvements to Java ● Better OO features ● Better functional features ● Better language features ● Repl (groovysh) ● Java features earlier ● Case Study

Slide 94

Slide 94 text

Repl: groovysh (completion)

Slide 95

Slide 95 text

Repl: groovysh (commands)

Slide 96

Slide 96 text

Repl: groovysh (/ls, /grep, /slurp, /cat, pipes)

Slide 97

Slide 97 text

Repl: groovysh (/nano)

Slide 98

Slide 98 text

Repl: groovysh (/grab, /slurp, /prnt)

Slide 99

Slide 99 text

Repl: groovysh (/grab, /prnt, /pipe, /classloader)

Slide 100

Slide 100 text

Repl: groovysh (/pipe, /show, /slurp)

Slide 101

Slide 101 text

Repl: groovysh (configurable colors & theming)

Slide 102

Slide 102 text

Repl: groovysh (widgets, e.g. tooltips)

Slide 103

Slide 103 text

Repl: groovysh (/ttop)

Slide 104

Slide 104 text

Agenda ● Top 5 Groovy features not in Java ● Top 5 Groovy improvements to Java ● Better OO features ● Better functional features ● Better language features ● Repl (groovysh) ● Java features earlier ● Case Study

Slide 105

Slide 105 text

Java Features Earlier Summary Feature JDK version with Java JDK version with Groovy Sealed types 17 8 emulated, 17 native Records 16 8 emulated/similar, 16 native Text blocks 15 8 equivalent Templating ? 8 Scripting JSR223 JEP512 compact classes - - 25 8 8 8 equivalent/improved, Groovy 5 Sequenced collections 21 8 equivalent Gatherers 24 8 includes similar non-stream functionality Switch improvements 14/21 8 similar functionality

Slide 106

Slide 106 text

Agenda ● Top 5 Groovy features not in Java ● Top 5 Groovy improvements to Java ● Case Study

Slide 107

Slide 107 text

Case Study: Roman Numerals I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000 • The value of a symbol is added to itself, as many times as it appears • A symbol can be repeated only three times • V, L, and D are never repeated • When a symbol of smaller value appears after a symbol of greater value, its values will be added • When a symbol of a smaller value appears before a greater value symbol, it will be subtracted

Slide 108

Slide 108 text

Case Study: Roman Numerals com.github.fracpete:romannumerals4j:0.0.1 Direct library usage var fmt = new RomanNumeralFormat() assert fmt.parse('XII') == 12 assert fmt.format(9) == 'IX' assert fmt.format(fmt.parse('XII') + fmt.parse('IX')) == 'XXI'

Slide 109

Slide 109 text

Case Study: Roman Numerals Using dynamic metaprogramming String.metaClass.toDecimal { fmt.parse(delegate) } Integer.metaClass.toRoman { fmt.format(delegate) } assert 'XII'.toDecimal() == 12 assert 9.toRoman() == 'IX' assert 'XII'.toDecimal() + 'IX'.toDecimal() == 'XXI'.toDecimal()

Slide 110

Slide 110 text

Case Study: Roman Numerals Using extension methods static Integer toDecimal(String self) { fmt.parse(self) } static String toRoman(Integer self) { fmt.format(self) } assert 'XII'.toDecimal() == 12 assert 9.toRoman() == 'IX' assert 'XII'.toDecimal() + 'IX'.toDecimal() == 'XXI'.toDecimal()

Slide 111

Slide 111 text

Case Study: Roman Numerals With a domain class class RomanNumeral { private fmt = new RomanNumeralFormat() private int d RomanNumeral(String s) { d = fmt.parse(s) } RomanNumeral(int d) { this.d = d } def plus(RomanNumeral other) { new RomanNumeral(d + other.d) } String toString() { fmt.format(d) } boolean equals(other) { d == other.d } }

Slide 112

Slide 112 text

Case Study: Roman Numerals And a lifecycle method hook: Gives improved coding experience: def propertyMissing(String p) { new RomanNumeral(p) } assert XII + IX == XXI

Slide 113

Slide 113 text

Case Study: Roman Numerals class RomanNumeral implements Comparable { private fmt = new RomanNumeralFormat() private int d RomanNumeral(String s) { d = fmt.parse(s) } RomanNumeral(int d) { this.d = d } def plus(RomanNumeral other) { new RomanNumeral(d + other.d) } def multiply(RomanNumeral other) { new RomanNumeral(d * other.d) } String toString() { fmt.format(d) } int compareTo(other) { d <=> other.d } boolean equals(other) { d == other.d } RomanNumeral next() { new RomanNumeral(d+1) } }

Slide 114

Slide 114 text

Case Study: Roman Numerals assert XII + IX == XXI assert [LVII + LVII, V * III, IV..(V+I)] == [ CXIV, XV, IV..VI] assert switch(L) { case L -> '50 exactly' case XLV..LV -> 'close to 50' default -> 'not close to 50' } == '50 exactly'

Slide 115

Slide 115 text

Case Study: Roman Numerals @Sortable @Canonical class RomanNumeral { private fmt = new RomanNumeralFormat() final int d RomanNumeral(String s) { d = fmt.parse(s.toUpperCase()) } RomanNumeral(int d) { this.d = d } def plus(RomanNumeral other) { new RomanNumeral(d + other.d) } def multiply(RomanNumeral other) { new RomanNumeral(d * other.d) } def power(RomanNumeral other) { new RomanNumeral(d ** other.d) } String toString() { fmt.format(d) } RomanNumeral next() { new RomanNumeral(d+1) } } assert [LVII + LVII, V * III, V ** II, IV..(V+I), [X, V, I].sort()] == [ cxiv, xv, xxv, iv..vi, [i, v, x] ]

Slide 116

Slide 116 text

Case Study: Roman Numerals 3999 is the biggest roman numeral, otherwise we violate the rule about never having more than 3 of the same character in succession, so this statement: Intentionally gives this runtime error: But we can use type checking and add a custom type checking extension … Caught: java.text.ParseException: Unparseable number: "MMMM" assert MMMCMXCIX + I == MMMM

Slide 117

Slide 117 text

Case Study: Roman Numerals Now we get this compilation error: [Static type checking] - Not a valid roman numeral: MMMM @ line 6, column 25. assert MMMCMXCIX + I == MMMM ^ unresolvedVariable { VariableExpression ve -> try { new RomanNumeral(ve.name) storeType(ve, classNodeFor(RomanNumeral)) } catch(ParseException unused) { addStaticTypeError("Not a valid roman numeral: $ve.name", ve) } handled = true }

Slide 118

Slide 118 text

What about 2026? • Annotations in more places ▪ @Parallel, @Invariant on for loops • Grapes based on Maven Resolver and Ivy • Java compatibility ▪ Module import declarations ▪ Additional destructuring • Improve REPL further • Performance • Spec improvements • Further subprojects, e.g. GPars

Slide 119

Slide 119 text

The Groovy future is looking good • Groovy still has many features not yet available in Java • Groovy adds much value to Java features • Groovy has unparalleled extensibility so you can add features if there are some which are missing and you would like • We shouldn’t be looking at Groovy replacing Java but rather use whichever make sense for the task at hand

Slide 120

Slide 120 text

Why use Groovy in 2025? Dr Paul King, VP Apache Groovy & Distinguished Engineer, Object Computing Apache Groovy: Repo: Slides: Twitter/X | Mastodon | Bluesky: https://groovy.apache.org/ https://groovy-lang.org/ https://github.com/paulk-asert/groovy-today https://speakerdeck.com/paulk/groovy-today @ApacheGroovy | @[email protected] | @groovy.apache.org Questions?

Slide 121

Slide 121 text

Bonus material • Better scripting • Sealed types • Sequenced collections • Gatherer-related functionality

Slide 122

Slide 122 text

JEP 512 Scripting: Java • Standard class: • With JEP 512 Scripting (JDK 25) public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } void main() { IO.println("Hello, World!"); }

Slide 123

Slide 123 text

JEP 512 Scripting: Groovy earlier versions • Standard class: • Static main method only: • Script: class HelloWorld { static main(args) { println 'Hello, World!' } } @CompileStatic static main(args) { println 'Hello, World!' } println 'Hello, World!' • Usually only used if you want to annotate the main method

Slide 124

Slide 124 text

JEP 512 Scripting: Groovy earlier versions • Standard class: • Static main method only: • Script: class HelloWorld { static main(args) { println 'Hello, World!' } } @CompileStatic static main(args) { println 'Hello, World!' } println 'Hello, World!' • Return type promoted to public void • Arguments promoted to String[] • Script class added • Has access to binding and context • Content added to run method • public static void main added creating instance and calling run • Variable declarations are local variables in run method or promoted to fields if annotated with @Field

Slide 125

Slide 125 text

JEP 512 Scripting: Groovy 5 • “Instance main” method: • “Instance run” method: @JsonIgnoreProperties(["binding"]) def run() { var mapper = new ObjectMapper() assert mapper.writeValueAsString(this) == '{"pets":["cat","dog"]}' } public pets = ['cat', 'dog'] def main() { assert upper(foo) + lower(bar) == 'FOObar' } def upper(s) { s.toUpperCase() } def lower = String::toLowerCase def (foo, bar) = ['Foo', 'Bar'] • Now also supported: o Instance main which are JEP 512 compatible o Instance run which remains a script • @Field now only needed for standard scripts

Slide 126

Slide 126 text

Scripting: compared to Java Earlier JDK versions JDK 25 Earlier Groovy versions Groovy 5 Traditional Java Class     Traditional Groovy Script     “instance run” script     “static main” script  JEP 512 Compact source files & instance main methods   “instance main” script    1 Promoted to standard “public static void main” 1 1 1 1 1 2 Access to script binding & context 2 2 2 2 3 Use new run protocol 4 Runnable on JDK11+ from Groovy 3 3 4 5 Uses @Field 5 5 5

Slide 127

Slide 127 text

Other improvements: Sealed types Java Sealed Type Groovy Emulated Sealed Type Groovy Native Sealed Type JDK version 17+ 8+ 17+ Recognized by Java, Groovy Groovy Java, Groovy “non-sealed” to reopen Required Optional Optional

Slide 128

Slide 128 text

Other improvements: Sequenced collections (JDK 21) First element Java Java with JEP-431 Groovy (JDK 8+) Groovy with JEP-431 List list.get(0) collection.getFirst() aggregate[0] aggregate.first() Adds: collection.first collection.getFirst() Deque deque.getFirst() Set set.iterator().next() or set.stream().findFirst().get() SortedSet set.first() array array[0] unchanged unchanged Last element Java Java with JEP-431 Groovy (JDK 8+) Groovy with JEP-431 List list.get(list.size()-1) collection.getLast() aggregate[-1] aggregate.last() Adds: collection.last collection.getLast() Deque deque.getLast() Set requires iterating through the set SortedSet set.last() array array[array.length-1] unchanged unchanged

Slide 129

Slide 129 text

Other improvements: Gatherer-related functionality Groovy (Collections) Java & Groovy (Streams) Java & Groovy with JEP485 JDK versions 8+ 8+ 24 Basic windowing take(n) drop(n) ranges limit(n) skip(n) - - - - Advanced windowing collate(n) collate(n, step) collate(n, step, truncate) chop(n1, n2, …) - - - - windowFixed(n) windowSliding(n) custom gatherer custom gatherer Inject/fold inject (homogeneous types) inject (heterogenous types) inits() tails() reduce() - - - - fold() custom gatherer custom gatherer Cumulative sum inits() and sum() Non-stream: Arrays.parallelPrefix() scan() More information: https://groovy.apache.org/blog/groovy-gatherers

Slide 130

Slide 130 text

Join us: groovy.apache.org