Unveränderliche Daten im Griff - JUG Saxony Day · 2018-10-17 · Manuel Mauky @manuel_mauky...

Preview:

Citation preview

Manuel Mauky@manuel_mauky

Unveränderliche Daten im GriffImmutable Data mit Java

Manuel Mauky@manuel_mauky

www.lestard.eugithub.com/lestard

Immutable Object

"An immutable object is an object whose state cannot be modified after it is created"

wikipedia

- Immutable Data und funktionale Programmierung?- Nutzen?- Herausforderungen?- Immutable Collections- Immutable Objekte- Optics- andere JVM-Sprachen

- Kotlin- Frege

Immutable Data und

Funktionale Programmierung?

Definition "pure Funktion"- Das Ergebnis der Funktion hängt nur von den Argumenten ab

- Bei gleichen Argumenten liefert die Funktion stets das gleiche Ergebnis

- Die Funktion produziert keine Seiteneffekte

class SomeClass {

int doSomething(String s) {return s.trim().length();

}

}

class SomeClass {private int x = 0;

int doSomething(String s) {x = 1;return s.trim().length();

}

}

class SomeClass {private final int x = 0;

int doSomething(String s) {x = 1; //compile errorreturn s.trim().length();

}

}

class SomeClass {private final Person person = new Person();

int doSomething(String s) {person.setName(s);

return s.trim().length();}

}

int x = 0;

Function<String, Integer> doSomething = (s) -> {

x = 1;

return s.trim().length();}

Person person = new Person();

Function<String, Integer> doSomething = (s) -> {

person.setName(s);

return s.trim().length();}

class SomeClass {

static int doSomething(String s, Person person) {person.setName(s);

return s.trim().length();}

}

Nutzen von Immutable Data

"Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more

secure."Joshua Bloch - Effective Java, second edition

Nachvollziehbarkeit von Code

Minimierung von Einflussfaktoren

SomeClass someObject = new SomeClass();

unpureMethod(someObject);

System.out.println(someObject); // ?

List<String> strings = …

int x = somePureFunction(strings);

int y = x + 15;

List<String> strings = …

int x = someOtherPureFunction(strings); // replace without worrying

int y = x + 15;

public String doSomething(List<Person> persons) {…

}

Was könnte diese Methode tun?

public String doSomething(List<Person> persons) {…

}

Was könnte diese Methode tun?

Möglichkeiten:- Klassen-Variablen (Felder) lesen, verändern, neu zuweisen- Parameter "persons":

- Elemente hinzufügen, löschen, ersetzen- Instanzen in der Liste verändern- Irgendwo im Code die Referenz neu belegen (person = new ArrayList<>())

- Andere Methoden aufrufen- Seiten-Effekte (z.B. Datenbank-Zugriffe) durchführen- ...

public static String doSomething(List<Person> persons) {…

}

Was könnte diese Methode tun?

Möglichkeiten:- Klassen-Variablen (Felder) lesen, verändern, neu zuweisen- Parameter "persons":

- Elemente hinzufügen, löschen, ersetzen- Instanzen in der Liste verändern- Irgendwo im Code die Referenz neu belegen (persons = new ArrayList<>())

- Andere Methoden aufrufen- Seiten-Effekte (z.B. Datenbank-Zugriffe) durchführen- ...

public static String doSomething(final List<Person> persons) {…

}

Was könnte diese Methode tun?

Möglichkeiten:- Klassen-Variablen (Felder) lesen, verändern, neu zuweisen- Parameter "persons":

- Elemente hinzufügen, löschen, ersetzen- Instanzen in der Liste verändern- Irgendwo im Code die Referenz neu belegen (persons = new ArrayList<>())

- Andere Methoden aufrufen- Seiten-Effekte (z.B. Datenbank-Zugriffe) durchführen- ...

public static String doSomething(final ImmutableList<Person> persons) {…

}

Was könnte diese Methode tun?

Möglichkeiten:- Klassen-Variablen (Felder) lesen, verändern, neu zuweisen- Parameter "persons":

- Elemente hinzufügen, löschen, ersetzen- Instanzen in der Liste verändern- Irgendwo im Code die Referenz neu belegen (person = new ArrayList<>())

- Andere Methoden aufrufen- Seiten-Effekte (z.B. Datenbank-Zugriffe) durchführen- ...

public static String doSomething(final ImmutableList<ImmutablePerson> persons) {…

}

Was könnte diese Methode tun?

Möglichkeiten:- Klassen-Variablen (Felder) lesen, verändern, neu zuweisen- Parameter "persons":

- Elemente hinzufügen, löschen, ersetzen- Instanzen in der Liste verändern- Irgendwo im Code die Referenz neu belegen (person = new ArrayList<>())

- Andere Methoden aufrufen- Seiten-Effekte (z.B. Datenbank-Zugriffe) durchführen- ...

DebuggingHerausfinden, wo, warum und von wem ein Objekt verändert wurde

Mutation ist easy aber nicht unbedingt simple.

→ Empfehlung: Rich Hickey Talk "Simple made Easy"

easy = schnell und einfach verwendbar

simple = Gegenteil von "Complex"

Immutables sind Thread-Safe

Historisierung

Anforderung:Verschiedene Versionsstände sollen persistiert werden

Anforderung:Verschiedene Versionsstände sollen persistiert werden

SomeObject object = new SomeObject();

object.setValue(15);

object.setValue(30);

Funktionale UI-Entwicklung:Redux

Zustandsbaum

Reducer-Funktion

Action

State reduce(State oldState, Action action) {...

}

ReduxUI- und Applikations-zustand wird als Immutable Objekt modelliert

Pure Reducer-Funktion bildet aktuellen Zustand auf neuen Zustand ab

UI rendert sich stets passend zum neuen Zustand

Herausforderungen

HerausforderungenAuch in funktionalen Programmen muss mit Datenänderung umgegangen werden

Statt Objekte zu verändern müssen Kopien erzeugt werden

Herausforderung:

1) Speicher- und Laufzeit-Performance?2) API zum Entwickeln?

Immutable Collections

List<String> mutableList = new ArrayList<>();mutableList.add("foo");mutableList.add("bar");

List<String> immutableList = Collections.unmodifiableList(mutableList);

List<String> mutableList = new ArrayList<>();mutableList.add("foo");mutableList.add("bar");

List<String> immutableList = Collections.unmodifiableList(mutableList);

System.out.println(immutableList.size()); // 2

List<String> mutableList = new ArrayList<>();mutableList.add("foo");mutableList.add("bar");

List<String> immutableList = Collections.unmodifiableList(mutableList);

immutableList.add("baz"); // UnsupportedOperationException

List<String> mutableList = new ArrayList<>();mutableList.add("foo");mutableList.add("bar");

List<String> immutableList = Collections.unmodifiableList(mutableList);

mutableList.add("baz");

System.out.println(immutableList.size()); //?

List<String> mutableList = new ArrayList<>();mutableList.add("foo");mutableList.add("bar");

List<String> immutableList = Collections.unmodifiableList(mutableList);

mutableList.add("baz");

System.out.println(immutableList.size()); //3

(formerly known as Java-Slang)

Vavr- Bibliothek für funktionale Programmierung mit Java

- u.a. Collections Library

- http://www.vavr.io/

import io.vavr.collection.List;

List<String> list = List.of("foo", "bar");

import io.vavr.collection.List;

List<String> list = List.of("foo", "bar");

List<String> newList = list.append("baz");

System.out.println(list.size()); // 2

Iterable

Traversable

Seq

IndexedSeq

Array CharSeq Vector Stack List QueueStream

LinearSeq

Iterable

Traversable

Set

SortedSet

LinkedHashSet HashSet TreeSet

Map

SortedMap

LinkedHashMap HashMap TreeMap

list = list.remove("foo");

list = list.remove("foo");

list = list.filter(x -> !x.isEmpty());

list = list.remove("foo");

list = list.filter(x -> !x.isEmpty());

List<Integer> lengths = list.map(x -> x.length());

list = list.remove("foo");

list = list.filter(x -> !x.isEmpty());

List<Integer> lengths = list.map(x -> x.length());

String folded = list.fold("", (a,b) -> a + "." + b);// ["a","b","c"] => "a.b.c"

Guava Collections

import com.google.common.collect.ImmutableList;

ImmutableList<String> list = ImmutableList.of("foo", "bar");

import com.google.common.collect.ImmutableList;

ImmutableList<String> list = ImmutableList.of("foo", "bar");

list = ImmutableList.builder().addAll(list).add("baz").build();

// remove?

- PCollections

- cli-ds (Clojure collections outside of Clojure)

- functional-java

Andere Bibliotheken

Immutable Objekte

public class Person {private String firstname;private String lastname;

public String getFirstname() {return firstname;

}

public void setFirstname(String name) {this.firstname = name;

}

public String getLastname() {return lastname;

}

public void setLastname(String name) {this.lastname = name;

}}

public class Person {private String firstname;private String lastname;

public String getFirstname() {return firstname;

}

public void setFirstname(String name) {this.firstname = name;

}

public String getLastname() {return lastname;

}

public void setLastname(String name) {this.lastname = name;

}}

public class Person {private String firstname;private String lastname;

public String getFirstname() {return firstname;

}

public String getLastname() {return lastname;

}}

public class Person {private final String firstname;private final String lastname;

public Person(String firstname, String lastname) {this.firstname = firstname;this.lastname = lastname;

}

public String getFirstname() {return firstname;

}

public String getLastname() {return lastname;

}}

public class Person {private final String firstname;private final String lastname;

public Person(String firstname, String lastname) {this.firstname = firstname;this.lastname = lastname;

}

// getters

public boolean equals(Object other) {// generate by IDE

}

public int hashCode() {// generate by IDE

}}

public class Student extends Person {private String studentId;

// constructor

public String getStudentId() {return studentId;

}}

public class Student extends Person {private String studentId;

// constructor

public String getStudentId() {return studentId;

}

public void setStudentId(String studentId) {this.studentId = studentId;

}}

public class Student extends Person {private String studentId;

// constructor

public String getStudentId() {return studentId;

}

public void setStudentId(String studentId) {this.studentId = studentId;

}}

...

Person p = new Student(...);

somePureFunction(p);

p.setStudentId("sima123");

public final class Person {private final String firstname;private final String lastname;

public Person(String firstname, String lastname) {this.firstname = firstname;this.lastname = lastname;

}

// getters + equals + hashCode

}

public final class Person {private final String firstname;private final String lastname;private final Address address;

public Person(String firstname, String lastname, Address address) {this.firstname = firstname;this.lastname = lastname;this.address = address;

}

public Address getAddress() {return address;

}

// getter + equals + hashCode}

Address address = new Address("Görlitz", "02826", "Berliner Straße", "15");

Person person = new Person("Luise", "Müller", address);

Address address = new Address("Görlitz", "02826", "Berliner Straße", "15");

Person person = new Person("Luise", "Müller", address);

person.getAddress().setZipCode("02827");

Wie geht man mit Veränderung um?

public final class Person {private final String firstname;private final String lastname;private final Address address;

public Person(String firstname, String lastname, Address address) {// ...

}

public Person withLastname(String newLastName) {// TODO implement

}

// getter + equals + hashCode}

public final class Person {private final String firstname;private final String lastname;private final Address address;

public Person(String firstname, String lastname, Address address) {// ...

}

public Person withLastname(String newLastname) {return new Person(this.firstname, newLastname, this.address);

}

// getter + equals + hashCode}

public final class Person {private final String firstname;private final String lastname;private final Address address;

public Person(String firstname, String lastname, Address address) {// ...

}

public Person withLastname(String newLastname) {if(this.lastname.equals(newLastname)) {

return this;} else {

return new Person(this.firstname, newLastname, this.address);}

}

// getter + equals + hashCode}

Address address = new Address("City", "Street", "House number");

Person person = new Person("Luise", "Müller", address);

Address address = new Address("City", "Street", "House number");

Person person = new Person("Luise", "Müller", address);

Person newPerson = person.withLastname("Maier");

Address address = new Address("City", "Street", "House number");

Person person = new Person("Luise", "Müller", address);

person = person.withLastname("Maier");

public final class Person {private final String firstname;private final String lastname;private final Address address;private final String emailAddress;

public Person(String firstname, String lastname, Address address) {// ...

}

public Person withLastname(String newLastname) {if(this.lastname.equals(newLastname)) {

return this;} else {

return new Person(this.firstname, newLastname, this.address);}

}

// getter + equals + hashCode}

public final class Person {private final String firstname;private final String lastname;private final Address address;private final String emailAddress;

public Person(String firstname, String lastname, Address address, String email) {// ...

}

public Person withLastname(String newLastname) {if(this.lastname.equals(newLastname)) {

return this;} else {

return new Person(this.firstname, newLastname, this.address);}

}

// getter + equals + hashCode}

public final class Person {private final String firstname;private final String lastname;private final Address address;private final String emailAddress;

public Person(String firstname, String lastname, Address address, String email) {// ...

}

public Person withLastname(String newLastname) {if(this.lastname.equals(newLastname)) {

return this;} else {

return new Person(this.firstname, newLastname, this.address);}

}

// getter + equals + hashCode}

Lombok

Lombok- lombok Abhängigkeit ins Projekt ziehen- Annotationen setzen- Lombok generiert Boilerplate-Code

https://projectlombok.org/

@Datapublic class BlogArticle {

String title;String text;Author author;

}

public class BlogArticle { String title; String text;

Author author;

public BlogArticle() { }

public String getTitle() { return this.title; }

public String getText() { return this.text; }

public Author getAuthor() { return this.author; }

public void setTitle(String title) { this.title = title; }

public void setText(String text) { this.text = text; }

public void setAuthor(Author author) { this.author = author; }

public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof BlogArticle)) return false; final BlogArticle other = (BlogArticle) o; if (!other.canEqual((Object) this)) return false; final Object this$title = this.getTitle(); final Object other$title = other.getTitle(); if (this$title == null ? other$title != null : !this$title.equals(other$title)) return false; final Object this$text = this.getText(); final Object other$text = other.getText(); if (this$text == null ? other$text != null : !this$text.equals(other$text)) return false; final Object this$author = this.getAuthor(); final Object other$author = other.getAuthor(); if (this$author == null ? other$author != null : !this$author.equals(other$author)) return false; return true; }

public int hashCode() { final int PRIME = 59; int result = 1; final Object $title = this.getTitle(); result = result * PRIME + ($title == null ? 43 : $title.hashCode()); final Object $text = this.getText(); result = result * PRIME + ($text == null ? 43 : $text.hashCode()); final Object $author = this.getAuthor(); result = result * PRIME + ($author == null ? 43 : $author.hashCode()); return result; }

protected boolean canEqual(Object other) { return other instanceof BlogArticle; }

public String toString() { return "BlogArticle(title=" + this.getTitle() + ", text=" + this.getText() + ", author=" + this.getAuthor() + ")"; }}

vs.

Constructor

Getter + Setter

Equals

HashCode

ToString

Lombok für Immutables@Valuepublic class BlogArticle {

String title;String text;Author author;

}

// Usage

BlogArticle article = new BlogArticle("title", "text", author);

System.out.println(article.getTitle());

Lombok für Immutables@Value@Witherpublic class BlogArticle {

String title;String text;Author author;

}

// Usage

BlogArticle article = new BlogArticle("title", "text", author);

article = article.withTitle("New title");

Lombok für Immutables@Value@Wither@Builderpublic class BlogArticle {

String title;String text;Author author;

}

// Usage

BlogArticle article = BlogArticle.builder().title("Title").text("Text").author(author).build();

Lombok für Immutables@Value@Wither@Builder(toBuilder = true)public class BlogArticle {

String title;String text;Author author;

}

// Usage

BlogArticle article = BlogArticle.builder().title("Title").text("Text").author(author).build();

article = article.toBuilder().title("New Title").build();

Lombok - Kombination mit Collections?@Value@Witherclass BlogArticle {

Seq<Comment> comments;}

// usage

BlogArticle article = new BlogArticle(List.empty());

BlogArtcle article = article.withComments(article.getComments().append(comment));

Lombok - Kombination mit Collections?@Value@Witherclass BlogArticle {

Seq<Comment> comments = List.empty();}

// usage

BlogArticle article = new BlogArticle();

BlogArtcle article = article.withComments(article.getComments().append(comment));

Lombok - Kombination mit Collections?@Value@Witherclass BlogArticle {

Seq<Comment> comments = List.empty();

BlogArticle addComments(Comment...comments) {// ?

}

BlogArticle removeComments(Comment...comments) {// ?

}}

// usage

BlogArticle article = new BlogArticle();

BlogArtcle article = article.addComments(comment);

Lombok- guter IDE support

- einfache Integration

- vermeidet Boilerplate

https://projectlombok.org/

- Features für Immutability begrenzt

- Manchmal Probleme im Build-Prozess

- Annotationen sind nicht composable

Immutables.org

Immutables"Java annotation processors to generate simple, safe and consistent value objects"

Setup wie Lombok

https://immutables.github.io/

@Value.Immutablepublic interface BlogArticle {

String getTitle();String getText();

Author getAuthor();

List<Comment> getComments();}

// generiertImmutableBlogArticle article = ImmutableBlogArticle.builder()

.title("Title")

.text("Text")

.build();

article = article.withTitle("New Title");

// generiertImmutableBlogArticle article = ImmutableBlogArticle.builder()

.title("Title")

.text("Text")

.build();

article = article.withComments(comment); // collections support

// generiertImmutableBlogArticle article = ImmutableBlogArticle.builder()

.title("Title")

.text("Text")

.build();

article.getComments().add(comment); // UnsupportedOperationException

// generiertImmutableBlogArticle article = ImmutableBlogArticle.builder()

.title("Title")

.text("Text")

.build();

article = ImmutableBlogArticle.builder().from(article).addComments(comment).build();

Immutables.org- Optimiert für unveränderliche Daten

- Umfangreiche Builder werden generiert

- einfache Integration

https://immutables.github.io/

- kein spezieller IDE Support

- funktioniert aber trotzdem ohne Probleme

Immutables.org - Other Features

Code-Style@Value.Immutable@Value.Style(

init = "set*",typeAbstract = {"Abstract*"},typeImmutable = "*"

)public interface AbstractBlogArticle {

String getTitle();String getText();

}

// generated Class without "Immutable" prefix

BlogArticle article = BlogArticle.Builder().setTitle("Title").setText("Content").build();

Modifiable@Value.Immutable@Value.Modifiableinterface BlogArticle {

String getTitle();String getText();

}

ModifiableBlogArticle modifiableArticle = ModifiableBlogArticle.create().setTitle("Title").setText("Text");

modifiableArticle.setTitle("New Title");

ImmutableBlogArticle article = modifiableArticle.toImmutable();

ImmutableBlogArticle article = …

ModifiableBlogArticle modifiableArticle = ModifiableBlogArticle.create().from(article);

modifiableArticle.setTitle("New Title");

article = modifiableArticle.toImmutable();

Deeply Nested Structures

Optics

Address address = new Address("Görlitz", "02826", "Berliner Straße", "15");

Person person = new Person("Luise", "Müller", address);

person.getAddress().setZipCode("02827"); // mutation

Address address = new Address("Görlitz", "02826", "Berliner Straße", "15");

Person person = new Person("Luise", "Müller", address);

person.getAddress().setZipCode("02827");

person = person.withAddress(person.getAddress().withZipCode("02827"));

City goerlitz = new City("Görlitz", "02826");

Address address = new Address(goerlitz, "Berliner Straße", "15");

Person person = new Person("Luise", "Müller", address);

person.withAddress(person.getAddress().withCity(person.getAddress().getCity().withZipCode("02827")));

Tief verschachtelte Strukturen?

LensesLens: Objekt, welches auf eine bestimmte Stelle einer Datenstruktur "zeigt".

Person person = new Person("Luise", "Müller", address);

Lens personLastnameLens = …

Person person = new Person("Luise", "Müller", address);

Lens personLastnameLens = …

String lastname = personLastnameLens.get(person);

System.out.println(lastname); // "Müller"

Person person = new Person("Luise", "Müller", address);

Lens personLastnameLens = …

Person person = new Person("Luise", "Müller", address);

Lens personLastnameLens = …

Person newPerson = personLastameLens.set("Maier").apply(person);

System.out.println(newPerson.getLastname()); // "Maier"

Person person = new Person("Luise", "Müller", address);

Lens personLastnameLens = …

Person newPerson = personLastameLens.set("Maier").apply(person);

System.out.println(newPerson.getLastname()); // "Maier"

System.out.println(person.getLastname()); // "Müller"

LensesLens: Objekt, welches auf eine bestimmte Stelle einer Datenstruktur "zeigt".

Kann Werte abrufen und "setzen" (Kopie mit geänderten Daten erzeugen)

LensesLens: Objekt, welches auf eine bestimmte Stelle einer Datenstruktur "schaut".

Kann Werte abrufen und "setzen" (Kopie mit geänderten Daten erzeugen)

Lenses sind composable!

Person person = new Person("Luise", "Müller", address);

Lens.lens(person -> person.getAddress(), …)

Person person = new Person("Luise", "Müller", address);

Lens.lens(Person::getAddress, …)

Person person = new Person("Luise", "Müller", address);

Lens.lens(Person::getAddress, address -> person -> person.withAddress(address))

Person person = new Person("Luise", "Müller", address);

Lens<Person, Address> personAddressLens = Lens.lens(Person::getAddress,

address -> person -> person.withAddress(address))

Person person = new Person("Luise", "Müller", address);

Lens<Person, Address> personAddressLens = …

Lens<Address, City> addressCityLens = …

Person person = new Person("Luise", "Müller", address);

Lens<Person, Address> personAddressLens = …

Lens<Address, City> addressCityLens = …

Lens<City, String> cityZipcodeLens = …

Person person = new Person("Luise", "Müller", address);

Lens<Person, Address> personAddressLens = …

Lens<Address, City> addressCityLens = …

Lens<City, String> cityZipcodeLens = …

Lens<Person, String> personZipcodeLens =personAddressLens.compose(addressCityLens).compose(cityZipcodeLens);

Person person = new Person("Luise", "Müller", address);

Lens<Person, Address> personAddressLens = …

Lens<Address, City> addressCityLens = …

Lens<City, String> cityZipcodeLens = …

Lens<Person, String> personZipcodeLens =personAddressLens.compose(addressCityLens).compose(cityZipcodeLens);

Person newPerson = personZipcodeLens.set("02827").apply(person);

System.out.println(newPerson.getAddress().getCity().getZipCode())); // "02827"

Hilfsmittel zum Einsehen und "Verändern" von immutable Objekten

Abstraktion von konkreten Pfad zum Ziel-Wert

→ Ändert sich die Struktur, muss nur die Lens angepasst werden, nicht derbenutzende Code

Optics

Lens: "zeigt" auf einen stets vorhandenen Wert

Prism: "zeigt" auf einen möglicherweise vorhandenen Wert (Optional)

...

Bibliothek: Functional-Java

Optics

Andere JVM-Sprachen

Kotlin

Kotlindata class BlogArticle (

val title: String,val text: String

)

Kotlindata class BlogArticle (

val title: String,val text: String

)

val article = BlogArticle(title="Title", text="Text")

val newArticle = article.copy(title="New Title")

Kotlin Collectionsval list = listOf("foo", "bar")

list.add("baz");

Kotlin Collectionsval list = listOf("foo", "bar")

list.add("baz");

val mutableList = mutableListOf("foo", "bar")

FregeHaskell for the JVM

data Person = Person { firstname :: String, lastname :: String }

data Person = Person { firstname :: String, lastname :: String }

luise = Person "Luise" "Müller"

data Person = Person { firstname :: String, lastname :: String }

luise = Person "Luise" "Müller"

hugo = Person { lastname = "Bauer", firstname = "Hugo" }

data Person = Person { firstname :: String, lastname :: String }

luise = Person "Luise" "Müller"

hugo = Person { lastname = "Bauer", firstname = "Hugo" }

luise2 = luise.{ lastname = "Maier" }

Java Value-Types?JEP 169 - Project Valhalla

Project Valhalla: Value-Types für JavaValue-Types existieren direkt auf dem Call Stack und nicht im Heap

Keine Identität → Immutable

Fokus auf Performance und Speicher-Nutzung (keine Dereferenzierung mehr nötig)

"Enable functional-style computation with pure data, for optimized parallel computations."

"Increase safety and security and decrease "defensive copying" in applications which must share structured data across trust boundaries."

Fragen?@manuel_mauky

github.com/lestardwww.lestard.eu

Recommended