Varför jag gillar språket Groovy

Programspråket Groovy kan (lite löst) uttryckas som Java++, dvs en naturlig utvidgning av Java syntaxen för att kortare och bättre lösa ett programmeringstekniskt problem.

Groovy är mitt favoritspråk, eftersom jag kan skriva Java program, som är korta och begripliga utan att förvilla mig i en massa syntaktisk jox. Så låt mig få visa ett konkret exempel på hur det går att korta ned ett Java program till ett mycket kortare Groovy program.

En kopp Java att starta med

Följande Java program hämtar en XML fil (från Brottplatskartan) med de senaste rapporterade brotten/olyckorna och listar trafikolyckorna i kronologisk ordning.

Programkoden i Java är på 150 rader och GitHub gist:en nedan visar både kod och utdata:
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilderFactory;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
public class CrimesList {
public static final String URL = "http://brottsplatskartan.se/api.php?action=getEvents&period=";
public static void main(String[] args) throws Exception {
int hours = args.length > 0 ? Integer.parseInt(args[0]) : 10;
String phrase = args.length > 1 ? args[1] : "trafikolycka";
CrimesList app = new CrimesList();
List<Crime> crimes = app.getCrimes(hours);
Set<Crime> crimesFiltered = app.filter(crimes, phrase);
System.out.printf("Det har inträffat %d händelser av typ '%s' de senaste %d timmarna.%n", crimesFiltered.size(), phrase, hours);
for (Crime crime : crimesFiltered) {
System.out.printf("%1$te %1$tb %1$tY, %1$tT - %2$s (%3$s)%n", crime.getDate(), crime.getTitle(), crime.getPlace());
}
}
Set<Crime> filter(List<Crime> crimes, String phrase) {
Set<Crime> result = new TreeSet<>();
for (Crime crime : crimes) {
if (crime.getTitle().toLowerCase().contains(phrase) || crime.getDescription().toLowerCase().contains(phrase)) {
result.add(crime);
}
}
return result;
}
public List<Crime> getCrimes(int lastHours) throws Exception {
String url = URL + (lastHours * 60);
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(url);
doc.getDocumentElement().normalize();
return xml2crime(doc.getElementsByTagName("event"));
}
private List<Crime> xml2crime(NodeList events) throws ParseException {
List<Crime> crimes = new ArrayList<>(events.getLength());
for (int i = 0; i < events.getLength(); i++) {
crimes.add( xml2crime(events.item(i)) );
}
return crimes;
}
private Crime xml2crime(Node item) throws ParseException {
Element e = (Element) item;
return new Crime(text(e,"title"), text(e,"date"), text(e,"place"), text(e,"text"));
}
private String text(Element e, String tag) {
return e.getElementsByTagName(tag).item(0).getTextContent();
}
public static class Crime implements Comparable<Crime> {
private String title, place, description;
private Date date;
private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public Crime(String title, String date, String place, String description) throws ParseException {
this(title, iso8601.parse(date), place, description);
}
public Crime(String title, Date date, String place, String description) {
this.title = title;
this.date = date;
this.place = place;
this.description = description;
}
@Override
public String toString() {
return "Crime{" +
"title='" + title + '\'' +
", date=" + date +
", place='" + place + '\'' +
", description='" + description + '\'' +
'}';
}
@Override
public int compareTo(Crime that) {
return this.getDate().compareTo(that.getDate());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Crime crime = (Crime) o;
if (date != null ? !date.equals(crime.date) : crime.date != null) return false;
if (description != null ? !description.equals(crime.description) : crime.description != null) return false;
if (place != null ? !place.equals(crime.place) : crime.place != null) return false;
if (title != null ? !title.equals(crime.title) : crime.title != null) return false;
return true;
}
@Override
public int hashCode() {
int result = title != null ? title.hashCode() : 0;
result = 31 * result + (date != null ? date.hashCode() : 0);
result = 31 * result + (place != null ? place.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
return result;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getPlace() {
return place;
}
public void setPlace(String place) {
this.place = place;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
}
view raw CrimesList.java hosted with ❤ by GitHub
Det har inträffat 11 händelser av typ 'trafikolycka' de senaste 10 timmarna.
28 jan 2013, 07:32:00 - Trafikolycka, personskada (E18, Trafikplats Hägernäs, Hägernäs, Täby)
28 jan 2013, 07:38:00 - Trafikolycka (Västerås)
28 jan 2013, 08:10:00 - Trafikolycka, singel (Anderstorp, Gislaved)
28 jan 2013, 09:45:00 - Trafikolycka, personskada (Årstaängsvägen, Liljeholmen, Stockholm)
28 jan 2013, 09:49:00 - Trafikolycka, vilt (Berg)
28 jan 2013, 09:53:00 - Trafikolycka, personskada (Östersund)
28 jan 2013, 11:04:00 - Trafikolycka (Coop, Härnösand)
28 jan 2013, 11:05:00 - Trafikolycka, singel (Personbil kör av vägen, Järavallen, Kävlinge)
28 jan 2013, 11:10:00 - Trafikolycka, personskada (Nettovägen, Veddesta, Järfälla)
28 jan 2013, 14:11:00 - Trafikolycka, personskada (Ankargatan, Västerås)
28 jan 2013, 14:49:00 - Trafikolycka, personskada (Moped, Jönköping)


Så här kompilerar och exekverar du Java programmet:
Yes> javac -d target/classes src/java/CrimesList.java
Yes> java -cp target/classes CrimesList

Lite mysigare med Groovy

I första steget byter jag namn på filen, så att filändelsen är *.groovy, samt kapar bort en del överflödigt Java jox.

Inga semi-kolon

Vi behöver inte semikolon, så bort med dem i slutet av varje programrad.

GroovyBeans

Det finns en inre JavaBean klass, som vi direkt kan göra om till en GroovyBean i stället. Vi behöver inte getters och setters, eftersom Groovy tillhandahåller dem för oss (kompilerade), om vi deklarerar data-innehållet som properties. Standard metoder som toString(), equals(), hashCode() låter vi kompilatorn generera via annoteringar. Vi utnyttjar också att Java-klassen Date har nya metoder, såsom parse() och passar på att använda spaceship-operatorn (<=>) för att köra compareTo() metoden. Detta gjorde att en JavaBean på nästan 90 rader kan kapas ned till 20 rader. Inte illa!

Lite mer finlir

I huvud-metoden main() gör jag bara några mindre justeringar, vad beträffar utskrifter och en liten closure-loop istf for-each dito.

Ett första resultat

Så här ser Groovy klassen ut (med utdata) och den består av endast 80 rader.
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import org.w3c.dom.*
import javax.xml.parsers.DocumentBuilderFactory
import java.text.ParseException
class CrimesList_shorter {
public static final String URL = "http://brottsplatskartan.se/api.php?action=getEvents&period="
public static void main(String[] args) throws Exception {
int hours = args.length > 0 ? Integer.parseInt(args[0]) : 10
String phrase = args.length > 1 ? args[1] : "trafikolycka"
CrimesList_shorter app = new CrimesList_shorter()
List<Crime> crimes = app.getCrimes(hours)
Set<Crime> crimesFiltered = app.filter(crimes, phrase)
println "Det har inträffat ${crimesFiltered.size()} händelser av typ '${phrase}' de senaste ${hours} timmarna."
crimesFiltered.each {crime ->
println "${crime.date.format('d MMM yyyy, HH:mm:ss')} - ${crime.title} (${crime.place})"
}
}
Set<Crime> filter(List<Crime> crimes, String phrase) {
Set<Crime> result = new TreeSet<>()
for (Crime crime : crimes) {
if (crime.title.toLowerCase().contains(phrase) || crime.description.toLowerCase().contains(phrase)) {
result.add(crime)
}
}
return result
}
public List<Crime> getCrimes(int lastHours) throws Exception {
String url = URL + (lastHours * 60)
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(url)
doc.documentElement.normalize()
return xml2crimeList(doc.getElementsByTagName("event"))
}
private List<Crime> xml2crimeList(org.w3c.dom.NodeList events) throws ParseException {
List<Crime> crimes = new ArrayList<>(events.length)
for (int i = 0; i < events.length; i++) {
crimes.add( xml2crime(events.item(i)) )
}
return crimes
}
private Crime xml2crime(org.w3c.dom.Node item) throws ParseException {
Element e = (Element) item
return new Crime(text(e,"title"), text(e,"date"), text(e,"place"), text(e,"text"))
}
private String text(Element e, String tag) {
return e.getElementsByTagName(tag).item(0).textContent
}
@ToString
@EqualsAndHashCode
public static class Crime implements Comparable<Crime> {
String title, place, description
Date date
public Crime(String title, String date, String place, String description) throws ParseException {
this(title, Date.parse('yyyy-MM-dd HH:mm:ss', date), place, description)
}
public Crime(String title, Date date, String place, String description) {
this.title = title
this.date = date
this.place = place
this.description = description
}
@Override
public int compareTo(Crime that) {
return this.date <=> that.date
}
}
}
Det har inträffat 14 händelser av typ 'trafikolycka' de senaste 10 timmarna.
28 jan 2013, 07:32:00 - Trafikolycka, personskada (E18, Trafikplats Hägernäs, Hägernäs, Täby)
28 jan 2013, 07:38:00 - Trafikolycka (Västerås)
28 jan 2013, 08:10:00 - Trafikolycka, singel (Anderstorp, Gislaved)
28 jan 2013, 09:45:00 - Trafikolycka, personskada (Årstaängsvägen, Liljeholmen, Stockholm)
28 jan 2013, 09:49:00 - Trafikolycka, vilt (Berg)
28 jan 2013, 09:53:00 - Trafikolycka, personskada (Östersund)
28 jan 2013, 11:04:00 - Trafikolycka (Coop, Härnösand)
28 jan 2013, 11:05:00 - Trafikolycka, singel (Personbil kör av vägen, Järavallen, Kävlinge)
28 jan 2013, 11:10:00 - Trafikolycka, personskada (Nettovägen, Veddesta, Järfälla)
28 jan 2013, 14:11:00 - Trafikolycka, personskada (Ankargatan, Västerås)
28 jan 2013, 14:49:00 - Trafikolycka, personskada (Moped, Jönköping)
28 jan 2013, 16:41:00 - Trafikolycka (Höjdgatan, Morö Backe, Skellefteå)
28 jan 2013, 16:49:00 - Trafikolycka (vilt, väg 92, Fredrika)
28 jan 2013, 16:56:00 - Trafikolycka (vilt, Degerås, Tvärålund)


Så här kompilerar och exekverar du Groovy programmet:
Yes> groovy src/groovy/CrimesList_shorter.groovy

Ännu mysigare Groovy

Men, det finns mer 'Java-jox' att kapa bort.

Groovy-Script

I Java måste ju all kod vara innesluten i en klass. I Groovy kan vi använda skript i stället. Så, bort med den omgivande applikations-klassen. Jag flyttar också omkring lite kod, så att koden i main() hamnar längsdt ned och Crime-bönan hamnar överst.

Bort med onödiga konstruktorer

Vad ska man med en konstruktor till egentligen? Groovy ger alla klasser utan konstruktor en speciell konstruktor, som tar namn-givna parametrar. Mycket tydligare, än parameter-positionen.
new Crime(title: e.title.text(), date: Date.parse('yyyy-MM-dd HH:mm:ss', e.date.text()), place: e.place.text(), description: e.text.text())

Mer Groovy

I det andra steget tar jag bort alla return-uttryck och använder mig av Groovy closures överallt där det tidigare fanns loopar.

Groovy XML-Parser

Slutligen, strutar jag i att använda Java's XML-DOM parser och kör med Groovy's egen som har en del finesser. Först, hämtar jag data och bygger upp ett XML träd.
new XmlParser().parse(url)

Från detta träd av XML noder, filtrerar jag ut alla 'event' noder, som en lista.
new XmlParser().parse(url).event

Denna node-lista bygger jag sedan om, så att varje node blir ett Crime objekt i stället.
new XmlParser().parse(url).event.collect {e -> new Crime(...)}

Slut-resultatet

Så, jag började med en Java klass på 150 rader och slutar här nedan med ett Groovy-script på blott 30 rader. Visst är det mysigt?
import groovy.transform.*
@ToString
@EqualsAndHashCode
class Crime implements Comparable<Crime> {
String title, place, description
Date date
public int compareTo(Crime that) { this.date <=> that.date }
}
List<Crime> getCrimes(int lastHours) throws Exception {
String url = "http://brottsplatskartan.se/api.php?action=getEvents&period=${lastHours * 60}"
new XmlParser().parse(url).event.collect {e ->
new Crime(title: e.title.text(), date: Date.parse('yyyy-MM-dd HH:mm:ss', e.date.text()), place: e.place.text(), description: e.text.text())
}
}
Set<Crime> filter(crimes, phrase) {
crimes.findAll { it.title.toLowerCase().contains(phrase) || it.description.toLowerCase().contains(phrase) } as TreeSet
}
int hours = args.length > 0 ? Integer.parseInt(args[0]) : 10
String phrase = args.length > 1 ? args[1] : "trafikolycka"
List<Crime> crimes = getCrimes(hours)
Set<Crime> crimesFiltered = filter(crimes, phrase)
println "Det har inträffat ${crimesFiltered.size()} händelser av typ '${phrase}' de senaste ${hours} timmarna."
crimesFiltered.each {crime ->
println "${crime.date.format('d MMM yyyy, HH:mm:ss')} - ${crime.title} (${crime.place})"
}
Det har inträffat 14 händelser av typ 'trafikolycka' de senaste 10 timmarna.
28 jan 2013, 07:32:00 - Trafikolycka, personskada (E18, Trafikplats Hägernäs, Hägernäs, Täby)
28 jan 2013, 07:38:00 - Trafikolycka (Västerås)
28 jan 2013, 08:10:00 - Trafikolycka, singel (Anderstorp, Gislaved)
28 jan 2013, 09:45:00 - Trafikolycka, personskada (Årstaängsvägen, Liljeholmen, Stockholm)
28 jan 2013, 09:49:00 - Trafikolycka, vilt (Berg)
28 jan 2013, 09:53:00 - Trafikolycka, personskada (Östersund)
28 jan 2013, 11:04:00 - Trafikolycka (Coop, Härnösand)
28 jan 2013, 11:05:00 - Trafikolycka, singel (Personbil kör av vägen, Järavallen, Kävlinge)
28 jan 2013, 11:10:00 - Trafikolycka, personskada (Nettovägen, Veddesta, Järfälla)
28 jan 2013, 14:11:00 - Trafikolycka, personskada (Ankargatan, Västerås)
28 jan 2013, 14:49:00 - Trafikolycka, personskada (Moped, Jönköping)
28 jan 2013, 16:41:00 - Trafikolycka (Höjdgatan, Morö Backe, Skellefteå)
28 jan 2013, 16:49:00 - Trafikolycka (vilt, väg 92, Fredrika)
28 jan 2013, 16:56:00 - Trafikolycka (vilt, Degerås, Tvärålund)


Så här kompilerar och exekverar du Groovy programmet:
Yes> groovy src/groovy/CrimesList_shortest.groovy

PS

Tråkigt nog, hann det inträffa några fler trafik-olyckor mellan att jag körde Java programmet och Groovy programmen.

2 kommentarer

Det finns också sätt att få samma/liknande förbättringar i Java. Se project lombok: http://peterisberg.com/blog/2012/12/06/project-lombok/ (ej spam).

Svara

Ja, project Lombok är helt klart inspirerat av Groovy. Mycket trevligt annotation-lib.

BTW, jag var också på jDays, men inte på detta seminarium. Jag höll f.ö. en kick-start Android kurs den tredje kursdagen.

Apropå annoteringar; jobbar man med Android utveckling, så är Android Annotations (http://androidannotations.org/) helt klart en stor hjälp.

Svara

Skicka en kommentar

Trevligt att du vill dela med dig av dina åsikter! Tänk på att hålla på "Netiketten" och använda vårdat språk.