Java Magazin   6.2021 - Internet of Things

Preis: 9,80 €

Erhältlich ab:  Mai 2021

Umfang:  100

Autoren / Autorinnen: 
Dominik Mohilo ,  
Tam Hanna ,  
Michael Simons ,  
Frank Delporte ,  
Frank Delporte ,  
Tam Hanna ,  
Dr. Veikko Krypczyk ,  
Karine VardanyanStephan Rauh ,  
Almas Baimagambetov ,  
Arne Limburg ,  
Elaine Barry ,  
Valentin SteinhauerLarissa Steinhauer ,  
Manfred Steyer ,  
Priyanka Shete ,  
Klaus Kurz ,  
Patrick McFadin ,  
,  
Redaktion . ,  
Redaktion .

Himbeeren sind großartig. Seit dem 16. Jahrhundert werden sie bereits kultiviert und heute wissen wir, dass das eine gute Entscheidung war: Vitamin A, C und E, Folsäure und Omega-3-Fettsäuren, Kalzium, Magnesium ... die Liste geht weiter. Mal davon abgesehen, dass Himbeeren auch ganz hervorragend schmecken, sei es im Salat, im alkoholischen Drink oder als Kuchen. Der Himbeerkuchen, im Englischen Raspberry Pie, war schließlich auch – so der Gründungsmythos – der Namenspate für den so erfolgreichen Mini-PC, der im Fokus dieser Ausgabe des Java Magazins steht.

Natürlich werden jetzt einige aufschreien und behaupten, dass Raspberry Pi, wie das kleine Kraftpaket offiziell heißt, sich auf einen Python-Interpreter bezieht. Dieser sollte ursprünglich den Unterschied zu den herkömmlichen und mit BASIC-Interpretern ausgestatteten Heimcomputern darstellen. Ob nun Kuchen oder Python (ob die wohl auch Himbeeren mögen?) – der Erfolg war dem raffinierten Stück Hardware in die Wiege gelegt. Über 36 Millionen verkaufte Exemplare sind im Umlauf, was die Beliebtheit unterstreicht. Nimmt man alle Raspberry Pis zusammen, kommt man auf ein Gesamtgewicht von rund 1 500 Tonnen, was natürlich nicht annähernd so viel ist, wie die 825 000 Tonnen Himbeeren, die wir Menschen pro Jahr produzieren und verschlingen, aber beeindruckend ist diese Zahl trotzdem. Der wohl schärfste Konkurrent, Arduino, kam bis 2013 auf gerade einmal 700 000 verkaufte Exemplare.

Heute ist der Raspberry Pi selbstverständlich ein sprachagnostisches „Spielzeug“ und fühlt sich damit auch im Internet of Things zuhause. Eine Sprache, die sich im Internet of Things eigentlich eher nicht so ganz heimisch fühlt, ist hingegen Java. Auch wenn die Behauptung, die Sprache sei zu „verbose“, zu schwergewichtig für das IoT, mittlerweile – auch Dank des Raspberry Pis – nicht mehr haltbar ist, ist es immer noch eher eine Nische, in der sich dieses Konstrukt bewegt. Hilfe bei der Realisierung von IoT-Anwendungen und dem Zugriff auf Sensoren über den Raspberry Pi mit Java gibt es in Form der Bibliothek Pi4J, über die Frank Delporte berichtet.

Mit diesen Werkzeugen an der Hand könnte man sich ja nun einen Roboter bauen, mit einem Raspberry Pi als Gehirn und Java als Sprache, der einem die Himbeeren aus dem Kühlschrank ins Wohnzimmer bringt. Wir freuen uns auf Erfahrungsberichte an die Redaktionsadresse!

Guten Appetit und viel Spaß bei der Lektüre!

mohilo_dominik_sw.tif_fmt1.jpgDominik Mohilo | Redakteur

Mail Website Twitter Xing

Brauchen wir immer neue Programmiersprachen? Müssen bestehende Programmiersprachen mit immer mehr Sprachfeatures erweitert werden? Muss jedes neue Sprachfeature sofort eingesetzt werden? Welche Sprachfeatures sollten verwendet werden? Wann ist der Umstieg sinnvoll? Diese Ausgabe der Kolumne wirft einen kritischen Blick auf neue Sprachen und neue Sprachfeatures in bestehenden Sprachen.

Die Verwendung neuer Programmiersprachen ist für viele Entwickler eine interessante Abwechslung im Programmieralltag. Für ein neues Projekt wird gerne eine neue Programmiersprache verwendet, ohne sich Gedanken zu machen, ob das überhaupt sinnvoll ist. Aber warum werden neue Sprachen (oder auch Sprachfeatures in bestehenden Sprachen) überhaupt erfunden und wann ist ihr Einsatz sinnvoll?

The Billion Dollar Mistake

Tony Hoare bezeichnet sich selbst als den Erfinder der Null Reference [1] und nennt das zugleich seinen Milliarden-Dollar-Fehler (Billion Dollar Mistake); einfach deshalb, weil er der Meinung ist, diese Erfindung hätte seither verschiedene Unternehmen aufgrund von dadurch entstehenden Programmierfehlern zwischen einer und zehn Milliarden Dollar gekostet. Nach seiner Aussage hat er die Nullreferenz beim Entwurf der Programmiersprache ALGOL erfunden, einfach, weil es so leicht zu implementieren war.

Da erscheint es doch sinnvoll, beim Sprachdesign etwas besser aufzupassen. Aber was heißt denn besser aufpassen? Wie hätte man ALGOL ohne Nullreferenz besser designen können, bzw. wie kann man aus den Fehlern von damals lernen und moderne Programmiersprachen so entwickeln, dass das Problem nicht mehr auftritt?

Keine NullPointerExceptions in Kotlin?

Beim Sprachdesign von Kotlin stand geringe Fehleranfälligkeit von vornherein im Fokus. Und so wurde auch das Thema der Nullreferenz direkt adressiert. In Kotlin muss daher direkt bei der Deklaration einer Variablen angegeben werden, ob diese den Wert null annehmen kann oder nicht.

Falls eine Variable den Wert null annehmen kann, darf auch nur auf die Variable zugegriffen werden, wenn vorher überprüft wurde, ob sie nicht null ist. Versucht man es dennoch, gibt es einen Kompilierfehler. Dank geschicktem Sprachdesign sind hier NullPointerExceptions zur Laufzeit praktisch ausgeschlossen (Es gibt allerdings hier nicht näher erwähnte Spezialfälle). Darüber hinaus gibt es in Kotlin verschiedene Syntaxfeatures, um den Null-Check möglichst einfach (also mit möglichst wenig Code) durchzuführen. So kann ich mit dem Safe-Call-Operator ?. auf ein tiefer liegendes Attribut mit einem Ausdruck zugreifen, auch wenn „auf dem Weg“ irgendein Wert null ist. So liefert der Ausdruck customer?.address?.city?.zipCode im Normalfall das gewünschte Ergebnis – und null, falls eines der Zwischenergebnisse null ist (falls also z. B. die Adresse nicht gesetzt ist). Eine NullPointerException kann hier nicht entstehen.

Muss ich dann in jeder Situation, in der ich auf ein nullable Attribut zugreife, den Safe-Call-Operator verwenden? Die Antwort ist nein. Wenn ich vorher im Code einen klassischen Null-Check mache, erkennt das der Compiler und der Safe-Call-Operator ist dann nicht nötig (Listing 1). Hier liegt allerdings der Teufel im Detail. Denn während der Compiler bei der lokalen Variable in Listing 1 sicher sein kann, dass der Wert im anschließenden Block niemals null werden kann, so wäre das bei einem Instanzfeld anders (dieses könnte z. B. durch den Methodenaufruf checkAddress() auf null gesetzt werden). Wenn also customer ein Feld des Objekts wäre, würde der Compiler den Zugriff ohne Safe-Call-Operator nicht erlauben. Um auch solche Zugriffe zu vereinfachen, gibt es in Kotlin die Scope Functions. So lässt sich z. B. in der Kombination aus Safe-Call-Operator und der Scope Function let Code implementieren, der nur ausgeführt wird, wenn der Kunde nicht null ist (Listing 2). Innerhalb des let-Codes kann über das Schlüsselwort it auf den Kunden zugegriffen werden. Dadurch kann Listing 2 auch verwendet werden, wenn es sich bei customer um eine Instanzvariable handelt.

Listing 1

val customer: Customer? = …
if (customer != null) {
  checkAddress()
  // Der Zugriff auf customer erfolgt ohne Safe Call Operator
  customer.validateAddress()
}

Listing 2

customer?.let {
  checkAddress()
  // Der Zugriff auf customer erfolgt ohne Safe Call Operator
  it.validateAddress()
}

Optionale Referenzen in Java

In Kotlin wurde folglich bereits beim Sprachdesign darauf geachtet, dass keine Nullreferenzen möglich sind. So was im Nachhinein in eine Sprache aufzunehmen, gestaltet sich allerdings schwieriger. Insbesondere gilt das für eine Sprache wie Java, wo sehr viel Wert auf Abwärtskompatibilität gelegt wird. Abwärtskompatibilität bedeutet in diesem Kontext, dass einerseits alter Source Code mit einem aktuellen Compiler kompilierbar sein soll und andererseits Bytecode, der mit einem alten Compiler kompiliert wurde, auch auf einer aktuellen JVM laufen soll.

Da lässt sich das Konzept der Nullreferenz nicht so einfach entfernen, ohne mit der Abwärtskompatibilität zu brechen. Es lassen sich aber Sprachkonstrukte einführen, die das Problem zumindest entschärfen. Genau das hat man in Java mit der Einführung von Optional versucht. Die Idee war, den Datentypen z. B. als Parameter zu verwenden, um zu signalisieren, dass dieser eben optional ist. Um auf den konkreten Wert eines Optional zuzugreifen, muss die Methode get() aufgerufen werden. Diese wirft eine Exception, wenn kein Wert enthalten ist. Wer sie aufruft, sollte also sicher sein, dass das Optional nicht leer ist. Überprüft werden kann das mit der Methode isPresent(). Wer also get() aufruft, ohne vorher mit isPresent() zu überprüfen, ob der Wert überhaupt vorhanden ist, handelt grob fahrlässig. Natürlich bleibt weiterhin das Problem, dass der Parameter, der das Optional enthält, selbst null sein kann. Dann wiederum handelt der Aufrufer grob fahrlässig. Natürlich geht es hier nicht um die Frage nach grober Fahrlässigkeit, und Lösungen wie in Kotlin, bei denen fehlerhafter Code in diesem Fall gar nicht möglich ist, sind natürlich zu bevorzugen. Trotzdem sollte unbestritten sein, dass Optional einen solchen Code weniger fehleranfällig macht – allein dadurch, dass man einem Parameter ansieht, dass er leer sein kann. Und die Verwendung von Optional sensibilisiert eben den Entwickler, sich Gedanken zu machen, was denn passieren soll, wenn der Wert nicht gesetzt ist.

Mit Optional lässt sich übrigens in Java auch etwas Ähnliches wie mit Scope Functions bei Kotlin realisieren, indem man den auszuführenden Code in ein Lambda packt und das Ganze an Optional.ifPresent übergibt (Listing 3).

Listing 3

Optional<Customer> customer = …;
customer.ifPresent(c -> {
  checkAddress();
  c.validateAddress();
});

Niemand mag Boilerplate-Code

Leider wird bei dem gezeigten Optional-Beispiel deutlich, dass es trotz Lambda mit recht viel Schreibarbeit verbunden ist, Code auf einem nullable Objekt auszuführen. Im Vergleich zu den Kotlin Scope Functions fällt vor allem auf, dass in Java eine Variablendeklaration inklusive weiterer Klammerung nötig ist, wo es in Kotlin einfach die Konvention mit dem Variablennamen it gibt.

Auch wenn das in diesem Beispiel durchaus als Jammern auf hohem Niveau bezeichnet werden kann (der Unterschied zwischen Java- und Kotlin-Code ist minimal), ist das Entfernen von Boilerplate-Code ein legitimes und sinnvolles Ziel beim Entwickeln neuer Sprachfeatures oder gar neuer Sprachen. Boilerplate-Code kann einerseits selbst Programmierfehler enthalten und lenkt vor allem von den wesentlichen Codeteilen ab, was das Finden von Programmierfehlern in den relevanten Teilen erschwert. Das Entfernen von Boilerplate-Code durch Sprachkonstrukte hat aber auch seine Nachteile, wie das Beispiel der Einführung des Streaming API zeigt.

Streams

Auch beim Durchsuchen, Filtern und Umsortieren von Listen (oder anderen Collections) entsteht häufig unübersichtlicher und schwer wartbarer Code. Da ist es nur konsequent, Sprachkonstrukte zur Verfügung zu stellen, die solche Operationen übersichtlicher und weniger fehleranfällig realisierbar machen. .NET-basierte Sprachen haben hier die standardisierte Abfragesprache LINQ. Sie lässt sich sogar direkt in den Quellcode schrei-ben. Hat man das Konzept einmal verstanden, sind solche Abfragen tatsächlich leichter zu lesen. LINQ ist eine Domain-specific Language für Abfragen. Dabei ist es sogar egal, ob die Abfrage gegen eine Datenbank oder gegen eine Liste erfolgen soll (Listing 4).

In Java wurden zur Lösung des Problems Streams eingeführt. Konzeptionell haben sie ein ähnliches Ziel wie LINQ, auch wenn bisher ein Framework fehlt, das Streams in DB-Abfragen umwandelt.

Streams legen aber zwei Probleme neuer Sprachkonstrukte offen, die deutlich werden, wenn man Listing 5 mit Listing 4 vergleicht:

Listing 4

IEnumerable<Customer> customersInCity = 
  from customer in customers
  from address in customer.addresses
  where listOfZipCodes.contains(address.zipCode)
  select customer;

Listing 5

List<Customer> customersInCity =
  customers.stream()
    .filter(
      c -> c.getAddresses().stream()
          .anyMatch(a -> listOfZipCodes.contains(a.getZipCode())))
.collect(Collectors.toList());
  1. Es ist nicht immer einfach, neue Sprachkonstrukte so zu gestalten, dass sie tatsächlich besser lesbar (und damit besser wartbar) sind.

  2. Wenn neue Sprachkonstrukte zu komplex sind, ist es schwierig, alle Entwickler darin zu schulen, sie zu verwenden.

Generics

Insbesondere der letzte Punkt ist mir das erste Mal im Zusammenhang mit dem Release von Java 5 und der damit einhergehenden Einführung von Generics begegnet. Während die gleichzeitige Einführung von Java-Enums relativ problemlos von jedem Entwickler sofort übernommen wurde, taten sie sich bei der Verwendung von Generics deutlich schwerer. Dass Listen und andere Collections mit Generics typisiert werden konnten, war recht schnell klar und erschien auch sinnvoll. Sobald aber ein Entwickler begann, im eigenen Code Generics zu verwenden, wurde das von anderen Entwicklern häufig gleich als Voodoo bezeichnet. Das lag sicherlich zu einem großen Teil daran, dass sie viele Entwickler tatsächlich einfach nicht verstanden, womit wir wieder beim Thema Schulungen wären.

Mittlerweile ist der Einsatz von Generics den meisten Entwicklern in Fleisch und Blut übergegangen und wo es sinnvoll ist, werden sie auch im eigenen Code verwendet (zugegebenermaßen sind das nicht viele Situationen). Dennoch verdeutlicht diese Erfahrung das Problem neuer Sprachfeatures, insbesondere, wenn ihnen eine gewisse Komplexität innewohnt.

Die Lösung ist natürlich nicht, sinnvolle Sprachfeatures nicht zu verwenden. Stattdessen sollten eben alle Entwickler regelmäßig darin geschult werden.

Fazit

Niemand mag Boilerplate-Code. Neben der Tatsache, dass es keinen Spaß macht, ihn zu schreiben (was man ggf. ja noch durch IDE-Templates lösten könnte), produziert er auch unnötigen Wartungsaufwand. Wenn man sich auf der Suche nach einem Fehler erst durch Tonnen von Boilerplate-Code wühlen muss, geht der Blick fürs Wesentliche (also die Fachlichkeit, die ggf. den Fehler enthält) schnell verloren, mal ganz davon abgesehen, dass natürlich auch Boilerplate-Code selbst Fehler enthalten kann.

Alles, was uns davon abhält, den Blick auf die Realisierung von Fachlogik zu lenken, sollten wir also vermeiden. Und da ist auch schon der Haken: Neue Sprachen und auch neue Features in bestehenden Sprachen können beides. Einerseits können sie sehr gut dazu geeignet sein, Boilerplate-Code zu vermeiden, andererseits birgt ein neues Sprachfeature auch immer die Gefahr, neuen unwartbaren Code zu schreiben, wenn das Feature falsch eingesetzt wird, wie bei den Streams gezeigt. Darüber hinaus besteht natürlich noch die Herausforderung, dass alle Entwickler das neue Sprachfeature kennen müssen, um den neu geschriebenen Code zu verstehen.

Beim Einsatz neuer Sprachen oder Sprachfeatures muss also einerseits sichergestellt werden, dass sie so eingesetzt werden, dass sie den Code leichter verständlich und weniger fehleranfällig machen. Andererseits muss aber auch klar sein, dass jedes Teammitglied das Sprachfeature oder die Sprache kennt und somit grundsätzlich in der Lage ist, den Code zu verstehen.

Entwickelt man mit IntelliJ Idea, gibt es für Entwickler häufig die Möglichkeit, Code, der ohne ein neues Sprachfeature (z. B. ohne Lambda) geschrieben ist, automatisch in „modernen“ Code umzuwandeln. Diese Möglichkeit ist zwar geeignet, Entwicklern unbekannte Sprachaspekte näherzubringen. Betrachtet ein solcher Entwickler aber bestehenden Code mit einem Sprachfeature, das er noch nicht kennt, hilft ihm das auch nicht weiter. In diesem Fall bräuchte er eher die Rückübersetzung in eine einfachere Version mit weniger Features. Aber vielleicht gibt es ja auch dafür in Zukunft eine Hilfestellung von der IDE.

In diesem Sinne – stay tuned.

limburg_arne_sw.tif_fmt1.jpgArne Limburg ist Softwarearchitekt bei der OPEN KNOWLEDGE GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.

Desktop Tablet Mobile
Desktop Tablet Mobile