What’s A Trait?

A trait ad-hoc combines features of several types into one

Some programming languages have a concept of traits, which allows creating a new type in the middle of a variable declaration that combines features of several types into one. Conceptionally, this could look as follows:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

typeMegacorp{

Stringname();

BigDecimal earnings();

}

traitIsSuccessful{

booleanisSuccessful(){

returnearnings().compareTo(SUCCESS_BOUNDARY)>0;

}

}

traitIsEvil{

booleanisEvil(){returntrue;}

}

Megacorp&IsSuccessful&IsEvil corp=

new(Megacorp&IsSuccessful&IsEvil)(/*...*/);

System.out.printf(

"Corporation %s is %s and %s.\n",

// relying on `corp` as `Megacorp`

corp.name(),

// relying on `corp` as `IsSuccessful`

corp.isSuccessful()?"successful":"a failure",

// relying on `corp` as `IsEvil`

corp.isEvil()?"evil":"a failure"

);

Looks nice, doesn’t it? If only the syntax wouldn’t be totally made up… But, as we will see in a minute, we can take a few steps into that direction with some trickery involving lambdas, default methods, and
var. Before we come to that, though, I want to address a question you might have at this point.

Wait, I Thought These Were Intersection Types!?

If this reminds you of intersection types, you’re on to something. The ampersand in the declaration has the same semantics: The resulting intersection inherits from the joined types and it follows that variables of that intersection type have the methods of all joined types – in this case of
Megacorp,
IsSuccessful, and
IsEvil.

The difference between intersection types and traits is that the former represent a requirement: A variable, parameter, or return value has to be of the intersection of the joined types. Traits, on the other hand, are about composing types: The variable created by the construction will be of the intersection of the joined types.

Naturally, intersection types and traits go well together:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

publicvoidcreateWithTraits(){

// create `corp` with two traits

Megacorp&IsSuccessful&IsEvil corp=

new(Megacorp&IsSuccessful&IsEvil)(/*...*/);

// call method that requires intersection

// of `Megacorp` and `IsEvil`

requireIntersection(corp)

}

publicvoidrequireIntersection(Megacorp&IsEvil corp){

// relying on `corp` as `Megacorp` and `IsEvil`

System.out.printf(

"Corporation %s is %s.\n",

corp.name(),

corp.isEvil()?"evil":"a failure"

);

}

Now, if only any of that were valid Java syntax… Let’s see how close we can get.

assign the entire kerfuffle to a
var-ed variable, so the type information don’t get trimmed to a single type

In the end, it will look like this:

1

2

// compiler infers desired intersection type for `corp`

varcorp=(MegacorpDelegate&IsSuccessful&IsEvil)()->megacorp;

I guess this leads us to the question: What the hell is going on here?

Let’s start with the initial type,
Megacorp in this example: It needs to be an interface. To intersect it with other types, an extending interface like
MegacorpDelegate needs to be created that default implements all methods by forwarding to a delegate:

1

2

3

4

5

6

7

8

9

10

11

interfaceMegacorp{

Stringname();

BigDecimal earnings();

}

@FunctionalInterface

interfaceMegacorpDelegateextendsMegacorp{

Megacorp delegate();

defaultStringname(){returndelegate().name();}

defaultBigDecimal earnings(){returndelegate().earnings();}

}

The delegating interface must have a single abstract method, which returns the delegate

It is critical that the delegating interface’s only abstract method is one that returns the delegate. This enables you to create an instance of it from a given
Megacorp with a lambda expression like
()->megacorp. Lambdas are crucial here because they are so-called poly expressions: They don’t have a defined type – instead it is inferred from the rest of the statement, in this case from the cast you place before them.

The delegating interface doesn’t add any domain functionality, it is only a technical requirement to make traits work in Java. As such, the delegating interface is conceptually close to the original one and I recommend placing it in the same package.

With that, some parts of the earlier declaration start to work:

1

2

3

Megacorp megacorp=// ...

// compiler infers `MegacorpDelegate` for `corp`

varcorp=(MegacorpDelegate)()->megacorp;

Creating such a delegating interface gives you the skeleton that you can now flesh out with the actual traits by writing interfaces that extend the original interface and add new methods.

Creating Traits

After creating a delegating interface like
MegacorpDelegate everything is set up for coding the actual traits. They add specific features to the original interface, here
Megacorp, that are apparently not general enough to become part it. That’s why traits are more likely to end up close to where they are needed instead of close to the original interface.

Trait interfaces must not have abstract methods

The second critical requirement is that the trait interfaces must not have abstract methods! If they do, their intersection with the delegating interface could not be created with a lambda.

Here are some example for our megacorporations:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

interfaceIsSuccessfulextendsMegacorp{

BigDecimal SUCCESS_BOUNDARY=newBigDecimal("500000000");

defaultbooleanisSuccessful(){

returnearnings().compareTo(SUCCESS_BOUNDARY)>0;

}

}

interfaceIsEvilextendsMegacorp{

defaultbooleanisEvil(){

returntrue;

}

}

Because
IsSuccessful and
IsEvil have no abstract methods, their intersection with
MegacorpDelegate can still be created with a lambda:

1

2

3

4

5

6

7

8

9

10

11

12

// compiles and runs

Megacorp megacorp=// ...

varcorp=(MegacorpDelegate&IsSuccessful&IsEvil)()->megacorp;

System.out.printf(

"Corporation %s is %s and %s.\n",

// relying on `Megacorp`

corp.name(),

// relying on `IsSuccessful`

corp.isSuccessful()?"successful":"a failure",

// relying on `IsEvil`

corp.isEvil()?"evil":"a failure"

);

Et voilà, given a
Megacorp instance you can decide on the spot which other functionality you want to compose with it. You can quickly and easily code that functionality up for your very specific context and start using it very naturally with plain method calls on the instance.

But the best thing is, neither the creators of
Megacorp,
MegacorpDelegate, or even
IsSuccessful and
IsEvil need to know what exactly you’re doing and neither do you need to create new types for any specific combination of traits. The latter is particularly important if you have many possible traits because the combinatorial explosion of creating a class for every combination would quickly overwhelm you.

The Dark Side

Ok, we’ve finally settled how to simulate traits in Java by using lambda expressions, default methods, intersection types, and local-variable type inference. This is a nice thought experiment and might even serve you as a stopgap one day, but it has severe disadvantages, which is why I don’t recommend to use it except in critical emergencies.

The first thing you might have noticed throughout this post is that simulating traits requires the combination of a decent number of non-trivial Java features, which always makes code less readable and thus less maintainable. Speaking of maintainability, variables of an intersection type only flourish within a method – as soon as they need to be passed to or from a method they require some generic trickery to be represented. This makes refactoring such code considerably more complicated.

There’s also some setup involved in creating the delegating interface and, even though that is unlikely to play a relevant role, the memory indirection it incurs reduces performance. Beyond that, traits need to be interfaces without abstract methods, which limits the functionality you can implement.

The final death knell is something else, though: Default methods can not implement
Object methods, which has the consequence that the combined instances can never forward a call to
equals or
hashCode to the underlying delegate. This is bound to break code that puts such instances in collections and will result in strange and hard-to-debug misbehavior.

So as much fun as playing with traits is, I strongly recommend never to simulate them this way. Alternatives are to simply gather the required methods in utility classes or to create subclasses after all.

Reflection

These are the ingredients to ad-hoc compose traits with a given interface:

a delegating interface extending the given interface:

uses default methods to forward all calls to delegate

has single abstract method
delegate(), so it can be created with
()->delegate

trait interfaces that have no abstract methods

declare variables as follows:

1

varvariable=(Interface&Trait1&Trait2)()->delegate;

This looks like a great way to avoid deep inheritance trees and instead compose the exact behavior you need exactly where you need. Alas, it has several downsides: