Strong Typing in Java: a religious argument

Strongly-typed, type-inferred languages like F#, Scala, and Haskell make Java feel like its static typing system is half-assed. This is not Java’s fault entirely; it’s the way we use it.

Say we have a domain class like


public class User {

public User(String firstName, String lastName, String email, long balanceInDollars) {}

...
}

I’m in agreement with Stephan that using String and primitive types here is wimping out. That is weak typing. Weak! Chuck Norris does not approve!

firstName and lastName and email are conceptually different. If each has their own type, then we get real compile-time type checking.


public class User {

public User(FirstName firstName, LastName lastName, EmailAddress email, Money balance) {}

...
}

This gives us type safety. As we pass a FirstName or an EmailAddress from class to class, we know what we’re getting. If we mix up the order of the arguments, we hear about it from the compiler.

“But they’re just strings!” you say. “Don’t make a bunch of cruft – just call them what they are!”

NO! I say. They are not Strings. They are stored as strings. That, my friend, is an implementation detail. FirstName and EmailAddress represent distinct concepts, and they should have distinct types. The first part of wisdom is calling things by their right names.

There are other OO-style benefits from this, such as putting the validation for each type in its type class and changing the internal representation of the type without affecting its interface. Those may be significant in some situations, but in my argument they’re icing. The benefit of strong typing is compile-time checking, and that’s reason enough to call a FirstName a FirstName and not a vague String.

Now, let’s address this “bunch of cruft” argument. No question, Java does not make this kind of type-wrapping pretty. In Haskell, it takes a one-line type alias. By OO principles, we ought to be able to inherit from String to get its behavior. But noooo, this is Java, and String is final, so we wind up with


public class FirstName {
public final String stringValue;

public FirstName(final String value) {
this.stringValue = value;
}

public String toString() {...}
public boolean equals() {...}
public int hashCode() {...}

}

(notice that I used a *gasp* public field. That’s another religious argument for another post. I include it here just to stir people up even further.)

Then every time we want a user:


User u = new User(new FirstName("Joe"), new LastName("Frank"), new EmailAddress("..."), new Money(30));

We can get a little better by providing static methods and statically importing them:


(in FirstName)

public static FirstName(final String value) {
return new FirstName(value);
}


User u = new User(FirstName("Joe"), LastName("Frank"), EmailAddress("..."), Money(30));

That’s a little better. But now we get into the part where user is an immutable value type, and we want to update one field.


User updated = new User("Joseph", old.getLastName(), old.getEmailAddress(), old.getBalance());

Ugh! and every time we add a new field to user, even though we simply want to copy it, we have to modify this code.
Copy constructors don’t help when the type is immutable.

Let’s talk about F# for a minute. F# has records:


type User = { firstName : FirstName; lastName : LastName ; email : EmailAddress; balance : Money }

and then the cool bit is, when you want another record that’s almost the same:


let updated = { oldUser with firstName = FirstName("Joseph") }

I want to do this with my Java user. I want to say


User updated = newUser.with(FirstName("Joseph"));

which is cool and all; with strong typing we can overload the “with” method all day long. We can chain them. We can add implementations of “with” for common combinations of fields to reduce instantiations.


(in User)

public User with(FirstName firstName) {
if (this.firstName.equals(firstName)) {
return this; // avoid instantiating an identical value object
}
return new User(firstName, this.lastName, this.email, this.balance);
}

Now you have a whole ton of “with” methods that can be chained. If you add a new field to User, you need to change all of them, but they’re all in the same place so that’s just fine. What changes together, stays together.

Now, if you don’t like an instantiation per changed field, or if you don’t like all those “with” methods cluttering up your user class, here’s another idea:


User updatedUser = new UserBuilder(oldUser).with(FirstName("Joseph").with(LastName("Frankenfurter").build();

where the UserBuilder keeps the oldUser and uses its values for any fields that aren’t provided by the caller to build the new user. That’s one instantiation, only one method that instantiates a user, and it’s encapsulated into one builder class.

Some people may argue that immutable types and strong typing in Java is going against the way the language is intended to be used, and therefore does nothing but make our lives more difficult. “That’s why God gave us POJOs,” they say. Java is a powerful language, and it is capable of supporting more idioms than the ones the language designers envisioned. We can grow as programmers within our language of choice. Java supports strong typing.

Strong typing gives us compiler errors on what would otherwise be caught only during testing. It creates some extra code, sure, but it’s localized. It can make working with immutable types a little cleaner.

I say, never use “String!”

52 thoughts on “Strong Typing in Java: a religious argument

Comments are closed.