blob: c593dd3c1e84fe7760271d2b8d846936dc1696fd [file] [log] [blame]
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]--><head>
<meta charset='utf-8'/><meta http-equiv='X-UA-Compatible' content='IE=edge'/><meta name='viewport' content='width=device-width, initial-scale=1'/><meta name='keywords' content='comparators, functional, gquery, ginq, groovy, lambdas, records, sorting'/><meta name='description' content='This post looks at Groovy functionality for making your classes comparable and/or sortable.'/><title>The Apache Groovy programming language - Blogs - Comparators and Sorting in Groovy</title><link href='../img/favicon.ico' type='image/x-ico' rel='icon'/><link rel='stylesheet' type='text/css' href='../css/bootstrap.css'/><link rel='stylesheet' type='text/css' href='../css/font-awesome.min.css'/><link rel='stylesheet' type='text/css' href='../css/style.css'/><link rel='stylesheet' type='text/css' href='https://cdnjs.cloudflare.com/ajax/libs/prettify/r298/prettify.min.css'/>
</head><body>
<div id='fork-me'>
<a href='https://github.com/apache/groovy'>
<img style='position: fixed; top: 20px; right: -58px; border: 0; z-index: 100; transform: rotate(45deg);' src='/img/horizontal-github-ribbon.png'/>
</a>
</div><div id='st-container' class='st-container st-effect-9'>
<nav class='st-menu st-effect-9' id='menu-12'>
<h2 class='icon icon-lab'>Socialize</h2><ul>
<li>
<a href='https://groovy-lang.org/mailing-lists.html' class='icon'><span class='fa fa-envelope'></span> Discuss on the mailing-list</a>
</li><li>
<a href='https://twitter.com/ApacheGroovy' class='icon'><span class='fa fa-twitter'></span> Groovy on Twitter</a>
</li><li>
<a href='https://groovy-lang.org/events.html' class='icon'><span class='fa fa-calendar'></span> Events and conferences</a>
</li><li>
<a href='https://github.com/apache/groovy' class='icon'><span class='fa fa-github'></span> Source code on GitHub</a>
</li><li>
<a href='https://groovy-lang.org/reporting-issues.html' class='icon'><span class='fa fa-bug'></span> Report issues in Jira</a>
</li><li>
<a href='http://stackoverflow.com/questions/tagged/groovy' class='icon'><span class='fa fa-stack-overflow'></span> Stack Overflow questions</a>
</li><li>
<a href='http://groovycommunity.com/' class='icon'><span class='fa fa-slack'></span> Slack Community</a>
</li>
</ul>
</nav><div class='st-pusher'>
<div class='st-content'>
<div class='st-content-inner'>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]--><div><div class='navbar navbar-default navbar-static-top' role='navigation'>
<div class='container'>
<div class='navbar-header'>
<button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'>
<span class='sr-only'></span><span class='icon-bar'></span><span class='icon-bar'></span><span class='icon-bar'></span>
</button><a class='navbar-brand' href='../index.html'>
<i class='fa fa-star'></i> Apache Groovy
</a>
</div><div class='navbar-collapse collapse'>
<ul class='nav navbar-nav navbar-right'>
<li class=''><a href='https://groovy-lang.org/learn.html'>Learn</a></li><li class=''><a href='https://groovy-lang.org/documentation.html'>Documentation</a></li><li class=''><a href='/download.html'>Download</a></li><li class=''><a href='https://groovy-lang.org/support.html'>Support</a></li><li class=''><a href='/'>Contribute</a></li><li class=''><a href='https://groovy-lang.org/ecosystem.html'>Ecosystem</a></li><li class=''><a href='/blog'>Blog posts</a></li><li class=''><a href='https://groovy.apache.org/events.html'></a></li><li>
<a data-effect='st-effect-9' class='st-trigger' href='#'>Socialize</a>
</li><li class=''>
<a href='../search.html'>
<i class='fa fa-search'></i>
</a>
</li>
</ul>
</div>
</div>
</div><div id='content' class='page-1'><div class='row'><div class='row-fluid'><div class='col-lg-3'><ul class='nav-sidebar'><li><a href='./'>Blog index</a></li><li class='active'><a href='#doc'>Comparators and Sorting in Groovy</a></li><li><a href='#_the_java_comparator_story_recap' class='anchor-link'>The Java comparator story recap</a></li><li><a href='#_the_groovy_comparator_story' class='anchor-link'>The Groovy comparator story</a></li><li><a href='#_mixing_in_some_language_integrated_queries' class='anchor-link'>Mixing in some language integrated queries</a></li><li><a href='#_conclusion' class='anchor-link'>Conclusion</a></li></ul><br/><ul class='nav-sidebar'><li style='padding: 0.35em 0.625em; background-color: #eee'><span>Related posts</span></li><li><a href='./parsing-json-with-groovy'>Parsing JSON with Groovy</a></li><li><a href='./groovy-list-processing-cheat-sheet'>Groovy List Processing Cheat Sheet</a></li><li><a href='./zipping-collections-with-groovy'>Zipping Collections with Groovy</a></li><li><a href='./groovy-records'>Groovy Records</a></li><li><a href='./deep-learning-and-eclipse-collections'>Deep Learning and Eclipse Collections</a></li><li><a href='./deck-of-cards-with-groovy'>Deck of cards with Groovy, JDK collections and Eclipse Collections</a></li><li><a href='./reading-and-writing-csv-files'>Reading and Writing CSV files with Groovy</a></li><li><a href='./using-groovy-with-apache-wayang'>Using Groovy with Apache Wayang and Apache Spark</a></li></ul></div><div class='col-lg-8 col-lg-pull-0'><a name='doc'></a><h1>Comparators and Sorting in Groovy</h1><p><span>Author: <i>Paul King</i></span><br/><span>Published: 2022-07-21 03:51PM</span></p><hr/><div id="preamble">
<div class="sectionbody">
<div class="paragraph">
<p><span class="image right"><img src="img/cher_record.png" alt="Cher" width="179" height="179"></span>
This blog post is inspired by the Comparator examples in the excellent
<em>Collections Refuelled</em> <a href="https://www.youtube.com/watch?v=q6zF3vf114M&amp;t=13s">talk</a> and <a href="https://blogs.oracle.com/java/post/collections-refueled">blog</a>
by Stuart Marks. That blog, from 2017, highlights improvements in the Java
Collections library in Java 8 and 9 including numerous Comparator improvements.
It is now 5 years old but still highly recommended for anyone using the Java
Collections library.</p>
</div>
<div class="paragraph">
<p>Rather than have a <code>Student</code> class as per the original blog example, we&#8217;ll have
a <code>Celebrity</code> class (and later record) which has the same <code>first</code> and <code>last</code>
name fields and an additional <code>age</code> field. We&#8217;ll compare initially by <code>last</code>
name with nulls before non-nulls and then by <code>first</code> name and lastly by <code>age</code>.</p>
</div>
<div class="paragraph">
<p>As with the original blog, we&#8217;ll cater for nulls,
e.g.&nbsp;a celebrity known by a single name.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_the_java_comparator_story_recap">The Java comparator story recap</h2>
<div class="sectionbody">
<div class="paragraph">
<p><span class="image right"><img src="img/JavaLogo.png" alt="Java logo" width="95" height="128"></span>
Our <code>Celebrity</code> class if we wrote it in Java would look something like:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="java">public class Celebrity { // Java
private String firstName;
private String lastName;
private int age;
public Celebrity(String firstName, int age) {
this(firstName, null, age);
}
public Celebrity(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "Celebrity{" +
"firstName='" + firstName +
(lastName == null ? "" : "', lastName='" + lastName) +
"', age=" + age +
'}';
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>It would look much nicer as a Java record (JDK16+) but we&#8217;ll keep with the spirit
of the original blog example for now. This is fairly standard boilerplate and in
fact was mostly generated by IntelliJ IDEA. The only slightly interesting aspect
is that we tweaked the <code>toString</code> method to not display null last names.</p>
</div>
<div class="paragraph">
<p>On JDK 8 with the old-style comparator coding, a main application which created
and sorted some celebrities might look like this:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="java">import java.util.ArrayList; // Java
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args) {
List&lt;Celebrity&gt; celebrities = new ArrayList&lt;&gt;();
celebrities.add(new Celebrity("Cher", "Wang", 63));
celebrities.add(new Celebrity("Cher", "Lloyd", 28));
celebrities.add(new Celebrity("Alex", "Lloyd", 47));
celebrities.add(new Celebrity("Alex", "Lloyd", 37));
celebrities.add(new Celebrity("Cher", 76));
Collections.sort(celebrities, (c1, c2) -&gt; {
String f1 = c1.getLastName();
String f2 = c2.getLastName();
int r1;
if (f1 == null) {
r1 = f2 == null ? 0 : -1;
} else {
r1 = f2 == null ? 1 : f1.compareTo(f2);
}
if (r1 != 0) {
return r1;
}
int r2 = c1.getFirstName().compareTo(c2.getFirstName());
if (r2 != 0) {
return r2;
}
return Integer.compare(c1.getAge(), c2.getAge());
});
System.out.println("Celebrities:");
celebrities.forEach(System.out::println);
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>When we run this example, the output looks like this:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>Celebrities:
Celebrity{firstName='Cher', age=76}
Celebrity{firstName='Alex', lastName='Lloyd', age=37}
Celebrity{firstName='Alex', lastName='Lloyd', age=47}
Celebrity{firstName='Cher', lastName='Lloyd', age=28}
Celebrity{firstName='Cher', lastName='Wang', age=63}</pre>
</div>
</div>
<div class="paragraph">
<p>As pointed out in the original blog, this code is rather tedious and error-prone and can be improved greatly with comparator improvements in JDK8:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="java">import java.util.Arrays; // Java
import java.util.List;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;
public class Main {
public static void main(String[] args) {
List&lt;Celebrity&gt; celebrities = Arrays.asList(
new Celebrity("Cher", "Wang", 63),
new Celebrity("Cher", "Lloyd", 28),
new Celebrity("Alex", "Lloyd", 47),
new Celebrity("Alex", "Lloyd", 37),
new Celebrity("Cher", 76));
celebrities.sort(comparing(Celebrity::getLastName, nullsFirst(naturalOrder())).
thenComparing(Celebrity::getFirstName).thenComparing(Celebrity::getAge));
System.out.println("Celebrities:");
celebrities.forEach(System.out::println);
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>The original blog also points out the convenience factory methods from JDK9 for
list creation which you might be tempted to consider here. For our case, we will
be sorting in place, so the immutable lists returned by those methods won&#8217;t help
us here but <code>Arrays.asList</code> isn&#8217;t much longer than <code>List.of</code> and works well for
this example.</p>
</div>
<div class="paragraph">
<p>As well as being much shorter, the <code>comparing</code> and <code>thenComparing</code> methods
and built-in comparators like <code>nullsFirst</code> and <code>naturalOrdering</code> allow for far
simpler composability. The sort within array list is also more efficient than
the sort that would have been used with the <code>Collections.sort</code> method on earlier
JDKs. The output when running the example is the same as previously.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_the_groovy_comparator_story">The Groovy comparator story</h2>
<div class="sectionbody">
<div class="paragraph">
<p><span class="image right"><img src="img/groovy_logo.png" alt="Groovy logo" width="180" height="90"></span>
At about the same time that Java was evolving its comparator story, Groovy
added some complementary features to tackle many of the same problems.
We&#8217;ll look at some of those features and also how the JDK improvements we
saw above features can be used instead if preferred.</p>
</div>
<div class="paragraph">
<p>First off, let&#8217;s create a Groovy <code>Celebrity</code> record:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">@Sortable(includes = 'last,first,age')
@ToString(ignoreNulls = true, includeNames = true)
record Celebrity(String first, String last = null, int age) {}</code></pre>
</div>
</div>
<div class="paragraph">
<p>And create our list of celebrities:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">var celebrities = [
new Celebrity("Cher", "Wang", 63),
new Celebrity("Cher", "Lloyd", 28),
new Celebrity("Alex", "Lloyd", 47),
new Celebrity("Alex", "Lloyd", 37),
new Celebrity(first: "Cher", age: 76)
]</code></pre>
</div>
</div>
<div class="paragraph">
<p>The record definition is nice and concise. It would look good in recent Java
versions too. A nice aspect of the Groovy solution is that it will provide
emulated records on earlier JDKs, and it also has some nice declarative
transforms to tweak the record definition. We could leave off the <code>@ToString</code>
annotation, and we&#8217;d get a standard record-style <code>toString</code>. Or we could add
a <code>toString</code> method to our record definition similar to what was done in
the Java example. Using <code>@ToString</code> allows us to remove null last names
from the <code>toString</code> in a declarative way. We&#8217;ll cover the <code>@Sortable</code>
annotation a little later.</p>
</div>
<div class="paragraph">
<p>First off, Groovy&#8217;s spaceship operator <code>&lt;&#8658;</code> allows us to write a nice compact
version of the "tedious" code in the first Java version. It looks like this:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">celebrities.sort { c1, c2 -&gt;
c1.last &lt;=&gt; c2.last ?: c1.first &lt;=&gt; c2.first ?: c1.age &lt;=&gt; c2.age
}
println 'Celebrities:\n' + celebrities.join('\n')</code></pre>
</div>
</div>
<div class="paragraph">
<p>And the output looks like this:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>Celebrities:
Celebrity(first:Cher, age:76)
Celebrity(first:Alex, last:Lloyd, age:37)
Celebrity(first:Alex, last:Lloyd, age:47)
Celebrity(first:Cher, last:Lloyd, age:28)
Celebrity(first:Cher, last:Wang, age:63)</pre>
</div>
</div>
<div class="paragraph">
<p>We&#8217;d have a tiny bit more work to do if we wanted nulls last but the defaults
work well for the example at hand.</p>
</div>
<div class="paragraph">
<p>We can alternatively, make use of the "new in JDK8" methods mentioned earlier:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">celebrities.sort(comparing(Celebrity::last, nullsFirst(naturalOrder())).
thenComparing(c -&gt; c.first).thenComparing(c -&gt; c.age))</code></pre>
</div>
</div>
<div class="paragraph">
<p>But this is where we should come back and further explain the <code>@Sortable</code>
annotation. That annotation is associated with an Abstract Syntax Tree (AST)
transformation, or just <em>transform</em> for short, which provides us with an
automatic <code>compareTo</code> method that takes into account the record&#8217;s properties
(and likewise if it was a class). Since we provided an <code>includes</code> annotation
attribute and provided a list of property names, the order of those names
determines the priority of the properties used in the comparator.
We could equally include just some of the names in that list or alternatively
provide a <code>excludes</code> annotation attribute and just mention that properties we
don&#8217;t want included.</p>
</div>
<div class="paragraph">
<p>It also adds <code>Comparable&lt;Celebrity&gt;</code> to the list of implemented interfaces
for our record. So, what does all this mean? It means we can just write:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">celebrities.sort()</code></pre>
</div>
</div>
<div class="paragraph">
<p>The transform associated with the <code>@Sortable</code> annotation
also provides some additional comparators for us.
To sort by age, we can use one of those comparators:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">celebrities.sort(Celebrity.comparatorByAge())</code></pre>
</div>
</div>
<div class="paragraph">
<p>Which gives this output:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>Celebrities:
Celebrity(first:Cher, last:Lloyd, age:28)
Celebrity(first:Alex, last:Lloyd, age:37)
Celebrity(first:Alex, last:Lloyd, age:47)
Celebrity(first:Cher, last:Wang, age:63)
Celebrity(first:Cher, age:76)</pre>
</div>
</div>
<div class="paragraph">
<p>In addition to the <code>sort</code> method, Groovy provides a <code>toSorted</code> method which
sorts a copy of the list, leaving the original unchanged. So, to create a list
sorted by first name we can use this code:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">var celebritiesByFirst = celebrities.toSorted(Celebrity.comparatorByFirst())</code></pre>
</div>
</div>
<div class="paragraph">
<p>Which, if output in a similar way to previous examples, gives:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>Celebrities:
Celebrity(first:Alex, last:Lloyd, age:37)
Celebrity(first:Alex, last:Lloyd, age:47)
Celebrity(first:Cher, last:Lloyd, age:28)
Celebrity(first:Cher, last:Wang, age:63)
Celebrity(first:Cher, age:76)</pre>
</div>
</div>
<div class="paragraph">
<p>If you are a fan of functional style programming, you might consider using <code>List.of</code> to define the original list and then only <code>toSorted</code> method calls in further processing.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_mixing_in_some_language_integrated_queries">Mixing in some language integrated queries</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Groovy also has a GQuery (aka GINQ) capability which provides a SQL inspired DSL
for working with collections. We can use GQueries to examine and order our
collection. Here is an example:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">println GQ {
from c in celebrities
select c.first, c.last, c.age
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Which has this output:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>+-------+-------+-----+
| first | last | age |
+-------+-------+-----+
| Cher | | 76 |
| Alex | Lloyd | 37 |
| Alex | Lloyd | 47 |
| Cher | Lloyd | 28 |
| Cher | Wang | 63 |
+-------+-------+-----+</pre>
</div>
</div>
<div class="paragraph">
<p>In this case, it&#8217;s using the natural ordering which <code>@Sortable</code> gives us.</p>
</div>
<div class="paragraph">
<p>Or we can sort by age:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">println GQ {
from c in celebrities
orderby c.age
select c.first, c.last, c.age
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Which has this output:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>+-------+-------+-----+
| first | last | age |
+-------+-------+-----+
| Cher | Lloyd | 28 |
| Alex | Lloyd | 37 |
| Alex | Lloyd | 47 |
| Cher | Wang | 63 |
| Cher | | 76 |
+-------+-------+-----+</pre>
</div>
</div>
<div class="paragraph">
<p>Or we can sort by last name descending and then age:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="prettyprint highlight"><code data-lang="groovy">println GQ {
from c in celebrities
orderby c.last in desc, c.age
select c.first, c.last, c.age
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Which has this output:</p>
</div>
<div class="listingblock">
<div class="content">
<pre>+-------+-------+-----+
| first | last | age |
+-------+-------+-----+
| Cher | Wang | 63 |
| Cher | Lloyd | 28 |
| Alex | Lloyd | 37 |
| Alex | Lloyd | 47 |
| Cher | | 76 |
+-------+-------+-----+</pre>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_conclusion">Conclusion</h2>
<div class="sectionbody">
<div class="paragraph">
<p>We have seen a little example of using comparators in Groovy. All the great JDK
capabilities are available as well as the spaceship operator, the <code>sort</code> and
<code>toSorted</code> methods, and the <code>@Sortable</code> AST transformation.</p>
</div>
</div>
</div></div></div></div></div><footer id='footer'>
<div class='row'>
<div class='colset-3-footer'>
<div class='col-1'>
<h1>Groovy</h1><ul>
<li><a href='https://groovy-lang.org/learn.html'>Learn</a></li><li><a href='https://groovy-lang.org/documentation.html'>Documentation</a></li><li><a href='/download.html'>Download</a></li><li><a href='https://groovy-lang.org/support.html'>Support</a></li><li><a href='/'>Contribute</a></li><li><a href='https://groovy-lang.org/ecosystem.html'>Ecosystem</a></li><li><a href='/blog'>Blog posts</a></li><li><a href='https://groovy.apache.org/events.html'></a></li>
</ul>
</div><div class='col-2'>
<h1>About</h1><ul>
<li><a href='https://github.com/apache/groovy'>Source code</a></li><li><a href='https://groovy-lang.org/security.html'>Security</a></li><li><a href='https://groovy-lang.org/learn.html#books'>Books</a></li><li><a href='https://groovy-lang.org/thanks.html'>Thanks</a></li><li><a href='http://www.apache.org/foundation/sponsorship.html'>Sponsorship</a></li><li><a href='https://groovy-lang.org/faq.html'>FAQ</a></li><li><a href='https://groovy-lang.org/search.html'>Search</a></li>
</ul>
</div><div class='col-3'>
<h1>Socialize</h1><ul>
<li><a href='https://groovy-lang.org/mailing-lists.html'>Discuss on the mailing-list</a></li><li><a href='https://twitter.com/ApacheGroovy'>Groovy on Twitter</a></li><li><a href='https://groovy-lang.org/events.html'>Events and conferences</a></li><li><a href='https://github.com/apache/groovy'>Source code on GitHub</a></li><li><a href='https://groovy-lang.org/reporting-issues.html'>Report issues in Jira</a></li><li><a href='http://stackoverflow.com/questions/tagged/groovy'>Stack Overflow questions</a></li><li><a href='http://groovycommunity.com/'>Slack Community</a></li>
</ul>
</div><div class='col-right'>
<p>
The Groovy programming language is supported by the <a href='http://www.apache.org'>Apache Software Foundation</a> and the Groovy community.
</p><div text-align='right'>
<img src='../img/asf_logo.png' title='The Apache Software Foundation' alt='The Apache Software Foundation' style='width:60%'/>
</div><p>Apache&reg; and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation.</p>
</div>
</div><div class='clearfix'>&copy; 2003-2023 the Apache Groovy project &mdash; Groovy is Open Source: <a href='http://www.apache.org/licenses/LICENSE-2.0.html' alt='Apache 2 License'>license</a>, <a href='https://privacy.apache.org/policies/privacy-policy-public.html'>privacy policy</a>.</div>
</div>
</footer></div>
</div>
</div>
</div>
</div><script src='../js/vendor/jquery-1.10.2.min.js' defer></script><script src='../js/vendor/classie.js' defer></script><script src='../js/vendor/bootstrap.js' defer></script><script src='../js/vendor/sidebarEffects.js' defer></script><script src='../js/vendor/modernizr-2.6.2.min.js' defer></script><script src='../js/plugins.js' defer></script><script src='https://cdnjs.cloudflare.com/ajax/libs/prettify/r298/prettify.min.js'></script><script>document.addEventListener('DOMContentLoaded',prettyPrint)</script><script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-257558-10', 'auto');
ga('send', 'pageview');
</script>
</body></html>