Practical Functional Java

Immutability

Jeff Butler

https://github.com/jeffgbutler/practical-functional-java

https://jeffgbutler.github.io/practical-functional-java/

It's All About Immutability

Functional programming imposes discipline on assignment.

Uncle Bob in "Clean Architecture", page 23

In other words...immutability is the key concept in functional programming.

"Functional" Java

  • Java is OO and we love changing the state of objects
  • Going immutable in Java takes discipline
  • Java 8 added some important things...
    • Functional Interfaces
    • Lambda expressions
    • Lazy Streams (a declarative paradigm)

Manifestations of Immutability

  • Immutable Classes
  • Pure Functions

If you can only remember one thing from this workshop, then remember that writing pure functions will radically change, and improve, the way you write code.

Immutable Classes

  • No Setters
  • Getters don't return mutable Objects like Lists or Maps
Simple Immutable Object
          
public final class SimpleImmutableObject {
    private final Integer id;
    private final String description;
    
    public SimpleImmutableObject(Integer id, String description) {
        this.id = Objects.requireNonNull(id);  // check for nulls!
        this.description = Objects.requireNonNull(description);
    }

    public Integer getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }
}
          
        
Instantiating Immutable Object
          
          SimpleImmutableObject myObject = new SimpleImmutableObject(3, "A Number");
          
        
Fluent Builder Pattern - More Flexible
          
public final class SimpleImmutableObjectWithBuilder {
    private final Integer id;
    private final String description;
    
    private SimpleImmutableObjectWithBuilder(Builder builder) {
        this.id = Objects.requireNonNull(builder.id);
        this.description = Objects.requireNonNull(builder.description);
    }

    // getters...

    public static class Builder {
        private Integer id;
        private String description;
        
        public Builder withId(Integer id) {
            this.id = id;
            return this;
        }
        
        public Builder withDescription(String description) {
            this.description = description;
            return this;
        }
        
        public SimpleImmutableObjectWithBuilder build() {
            return new SimpleImmutableObjectWithBuilder(this);
        }
    }
          
        
Instantiating With a Builder
          
  SimpleImmutableObjectWithBuilder myObject = new SimpleImmutableObjectWithBuilder.Builder()
      .withId(3)
      .withDescription("A Number")
      .build();
          
        

The with* methods can be called in any order, or multiple times, or not at all. The constructor should do null checking for required attributes.

Convenience "of" Methods
          
public final class SimpleImmutableObjectWithBuilder {
    ...
    
    public static SimpleImmutableObjectWithBuilder of(Integer id, String description) {
        return new Builder()
                .withId(id)
                .withDescription(description)
                .build();
    }
}
          
        
Instantiating With an "of" Method
          
SimpleImmutableObjectWithBuilder myObject = SimpleImmutableObjectWithBuilder.of(3, "A Number");
          
        

These "of" convenience methods can make immutable objects easier to use. You could write several methods like this to handle common cases.

Fluent Builder Pattern - How to Handle Optionals
          
public class ImmutablePerson {
    private String middleName;
    ...
    
    private ImmutablePerson(Builder builder) {
        ....
        middleName = builder.middleName;  // no null check
    }
   ...
   
    public Optional<String> getMiddleName() {
        return Optional.ofNullable(middleName);
    }
    
    public static class Builder {
        private String middleName;
        ...

        public Builder withMiddleName(String middleName) {
            this.middleName = middleName;
            return this;
        }
        ...
    }
}
          
        
Fluent Builder Pattern - How to Handle Lists
          
public class ImmutablePerson {
    private List<String> nickNames;
    ...
    
    private SimpleImmutableObjectWithBuilder(Builder builder) {
        ....
        nickNames = Objects.requireNonNull(builder.nickNames);
    }
   ...
   
    public Stream<String> nickNames() {
        return nickNames.stream();
    }
    
    public static class Builder {
        private List<String> nickNames = new ArrayList<>();
        ...

        public Builder withNickName(String nickName) {
            nickNames.add(nickName);
            return this;
        }
        
        public Builder withNickNames(List<String> nickNames) {
            this.nickNames.addAll(nickNames);
            return this;
        }
        ...
    }
}
          
        
How to Make "Mutable" Immutables
          
public class ImmutablePerson {
    ...
    
    // copy constructor - for the with*** methods
    private ImmutablePerson(ImmutablePerson other) {
        firstName = other.firstName;
        middleName = other.middleName;
        lastName = other.lastName;
        nickNames = new ArrayList<>(other.nickNames);
    }
    
    public ImmutablePerson withMiddleName(String middleName) {
        ImmutablePerson copy = new ImmutablePerson(this);
        copy.middleName = middleName;
        return copy;
    }
}
          
        
Using with* Methods
          
  ImmutablePerson person = new ImmutablePerson.Builder()
      .withFirstName("Fred")
      .withLastName("Flintstone")
      .build();
      
  person = person.withMiddleName("Farnsworth");
          
        

The with* methods create new instances. So the old person object still exists and is unchanged.

Pure Functions

  • Produce output based solely on their inputs
  • Do not rely on external state
  • Do not modify external state
  • Have no side effects
  • Are easier to reason about
  • Are easier to test
  • Are easier to multi-thread
Don't Do This!
          
    @Test
    public void testImpureFunction() {
        List<ImmutablePerson> allPeople = new ArrayList<>();
        addTheFlintstones(allPeople);
        addTheRubbles(allPeople);
        
        assertThat(allPeople.size()).isEqualTo(4);
        assertThat(allPeople.get(1).getFirstName()).isEqualTo("Wilma");
        assertThat(allPeople.get(3).getFirstName()).isEqualTo("Betty");
    }

    private void addTheFlintstones(List<ImmutablePerson> people) {
        people.add(ImmutablePerson.of("Fred", "Flintstone"));
        people.add(ImmutablePerson.of("Wilma", "Flintstone"));
    }

    private void addTheRubbles(List<ImmutablePerson> people) {
        people.add(ImmutablePerson.of("Barney", "Rubble"));
        people.add(ImmutablePerson.of("Betty", "Rubble"));
    }
          
        
Do This!
          
    @Test
    public void testPureFunction() {
        List<ImmutablePerson> allPeople = new ArrayList<>();
        allPeople.addAll(getTheFlintstones());
        allPeople.addAll(getTheRubbles());
        
        assertThat(allPeople.size()).isEqualTo(4);
    }
    
    private List<ImmutablePerson> getTheFlintstones() {
        List<ImmutablePerson> flintstones = new ArrayList<>();
        flintstones.add(ImmutablePerson.of("Fred", "Flintstone"));
        flintstones.add(ImmutablePerson.of("Wilma", "Flintstone"));
        return flintstones;
    }

    private List<ImmutablePerson> getTheRubbles() {
        List<ImmutablePerson> rubbles = new ArrayList<>();
        rubbles.add(ImmutablePerson.of("Barney", "Rubble"));
        rubbles.add(ImmutablePerson.of("Betty", "Rubble"));
        return rubbles;
    }
          
        
Even Better - Do This!
          
    @Test
    public void testPureFunction() {
        List<ImmutablePerson> allPeople = Stream.of(getTheFlintstones(), getTheRubbles())
                .flatMap(Function.identity())
                .collect(Collectors.toList());
        
        assertThat(allPeople.size()).isEqualTo(4);
        assertThat(allPeople.get(1).getFirstName()).isEqualTo("Wilma");
        assertThat(allPeople.get(3).getFirstName()).isEqualTo("Betty");
    }
    
    private Stream<ImmutablePerson> getTheFlintstones() {
        return Stream.of(ImmutablePerson.of("Fred", "Flintstone"),
                ImmutablePerson.of("Wilma", "Flintstone"));
    }

    private Stream<ImmutablePerson> getTheRubbles() {
        return Stream.of(ImmutablePerson.of("Barney", "Rubble"),
                ImmutablePerson.of("Betty", "Rubble"));
    }
          
        

Immutability Summary

This is hard to explain - you'll have to take my word on it... When you program with immutable objects and pure functions it changes the way you think.

Immutability Exercise

Exercise Instructions