
Today we are going to discuss how to implement generics in your code in a most effective way.
Don't Use Raw Types
This statement seems obvious. Raw types break the whole idea of generics. Its usage doesn't allow the compiler to detect type errors. But that’s not the only problem. Suppose we have such class.
classContainer<T>{privatefinalTvalue;...publicList<Integer>getNumbers(){returnnumbersList;}}
Assume that we don’t care about the generic type itself. All we need to do is to traversenumbersList
.
publicvoidtraverseNumbersList(Containercontainer){for(intnum:container.getNumbers()){System.out.println(num);}}
Surprisingly this code doesn’t compile.
error: incompatible types: Object cannot be converted to int for (int num : container.getNumbers()) { ^
The thing is that the raw type's usage erases not only the information about the generic type of a class but even predefined ones. So,List<Integer>
becomes just List.
What can we do about it? The answer is straightforward. If you don’t care about the generic type, use thewildcard operator.
publicvoidtraverseNumbersList(Container<?>container){for(intnum:container.getNumbers()){System.out.println(num);}}
This code snippet works perfectly fine.
Prefer Wildcard-Based Inputs
The main difference between arrays and generics is that arrays arecovariant while generics are not. It means thatNumber[]
is a supertype forInteger[]
. AndObject[]
is a supertype for any array (except primitive ones). That seems logical, but it may lead to bugs at runtime.
Number[]nums=newLong[3];nums[0]=1L;nums[1]=2L;nums[2]=3L;Object[]objs=nums;objs[2]="ArrayStoreException happens here";
This code does compile but it throws an unexpected exception. Generics were brought to solve this problem.
List<Number>nums=newArrayList<Number>();List<Long>longs=newArrayList<Long>();nums=longs;// compilation error
List<Long>
cannot be assigned toList<Number>
. Though it helps us to avoidArrayStoreException
, it also puts bounds that can make API not flexible and too strict.
interfaceEmployee{MoneygetSalary();}interfaceEmployeeService{MoneycalculateAvgSalary(Collection<Employee>employees);}
Everything looks good, isn’t it? We have even putCollection
providently as an input parameter. That allows us to passList
,Set
,Queue
, etc. But don’t forget thatEmployee
is just an interface. What if we worked with collections of particular implementations? For example,List<Manager>
orSet<Accountant>
? We couldn't pass them directly. So, it would require to shift the elements to the collection ofEmployee
type each time.
Or we can use the wildcard operator.
interfaceEmployeeService{MoneycalculateAvgSalary(Collection<?extendsEmployee>employees);}List<Manager>managers=...;Set<Accountant>accountants=...;Collection<SoftwareEngineer>engineers=...;// All these examples compile successfullyemployeeService.calculateAvgSalary(managers);employeeService.calculateAvgSalary(accountants);employeeService.calculateAvgSalary(engineers);
As you can see, the proper generic usage makes the life of a programmer much easier. Let’s see another example.
Suppose we need to declare an API for the sorting service. Here is the first naive attempt.
interfaceSortingService{<T>voidsort(List<T>list,Comparator<T>comparator);}
Now we’ve got a different kind of a problem. We have to be sure thatComparator
was created exactly for theT
type. But that’s not always true. We could build a universal one forEmployee
which wouldn't work for eitherAccountant
orManager
in this case.
Let’s make the API a bit better.
interfaceSortingService{<T>voidsort(List<T>list,Comparator<?superT>comparator);}// universal comparatorComparator<Employee>comparator=...;List<Manager>managers=...;List<Accountant>accountants=...;List<SoftwareEngineer>engineers=...;// All these examples compile successfulllysortingService.sort(managers,comparator);sortingService.sort(accountants,comparator);sortingService.sort(engineers,comparator);
You know, the constraints are a little bit confusing. All these? extends T
and? super T
seems overcomplicated. Thankfully there is an easy rule that can help to identify the correct usage —PECS (producer-extends, consumer-super). It means that a producer should be of type? extends T
while consumer of? super T
one.
Let’s take a look at particular examples. The methodMoneyService.calculateAvgSalary
that we described earlier accepts a producer. Because the collectionproduces elements that are used for further computations.
Another example comes right from the JDK standard library. I’m talking aboutCollection.addAll
method.
interfaceCollection<E>{booleanaddAll(Collection<?extendsE>c);...}
Defining upper bound generic allows us to concatenateCollection<Employee
> andCollection<Manager>
or any other classes that share the same interface.
What about consumers?Comparator
that we used inSortingService
is a perfect example. This interface has one method that accepts a generic type and returns a concrete one. A typical example of a consumer. Other ones arePredicate
,Consumer
,Comparable
, and many others fromjava.util
package. Mostly all of these interfaces should be used with? super T
bound.
There is also a unique one that is a producer and a consumer at the same time. It’sjava.util.Function
. It converts the input value from one type to another. So, the commonFunction usage isFunction<? super T, ? extends R>
. That may look scary but it really helps to build robust software. You can find out that all mapping functions inStream
interface follow this rule.
interfaceStream<T>extendsBaseStream<T,Stream<T>>{<R>Stream<R>map(Function<?superT,?extendsR>mapper);<R>Stream<R>flatMap(Function<?superT,?extendsStream<?extendsR>>mapper);IntStreamflatMapToInt(Function<?superT,?extendsIntStream>mapper);...}
One may notice that
SortingService.sort
acceptsList<T>
instead
ofList<? extends T>
. Why is it so? This is a producer after
all. Well, the thing is that upper and lowerbounds make sense in
comparing to the predefined type. BecauseSortingService.sort
method parameterizes itself, there is no sense to restrictList
with additional bounds. On the other hand, ifSortingService
had a generic type,? extends T
would have its value.
interfaceSortingService<T>{voidsort(List<?extendsT>list,Comparator<?superT>comparator);}
Don't Return Bounded Containers
Upper Bounds
Some developers that discovered the power of bounded generic types may consider that it’s a silver bullet. That can lead to the code snippets like the next one.
interfaceEmployeeRepository{List<?extendsEmployee>findEmployeesByNameLike(StringnameLike);}
What’s wrong here? Firstly,List<? extends Employee>
cannot be assigned toList<Employee>
without casting. More than that, this upper bound puts restrictions which are not obvious.
For example, values of what type can we put inside the collection returned byEmployeeRepository.findEmployeesByNameLike(String)
? You may suggest that it’s something likeAccountant
,Manager
,SoftwareEngineer
, and so on. But it’s a wrong assumption.
List<?extendsEmployee>employees=employeeRepository.findEmployeesByNameLike(nameLike);employees.add(newAccountant());// compile erroremployees.add(newSoftwareEngineer());// compile erroremployees.add(newManager());// compile erroremployees.add(null);// passes successfully 👍
This code snippet looks counter-intuitive but in reality, everything works just fine. Let’s deconstruct this case. First of all, we need to determine what collections can be assigned toList<? extends Employee>
.
List<?extendsEmployee>accountants=newArrayList<Accountant>();List<?extendsEmployee>managers=newArrayList<Manager>();List<?extendsEmployee>engineers=newArrayList<SoftwareEngineer>();// ...any other type that extends from Employee
So, basically list of any type that inherits fromEmployee
can be assigned toList<? extends Employee>
. This makes adding new items tricky. The compiler cannot be aware of the exact type of the list. That’s why it forbids to add any items in order to eliminate potential heap pollution. Butnull
is a special case. This value does not have its own type. It can be assigned to anything (except primitives). It is the reason whynull
is the only allowed value to add.
What about retrieving items from the list?
List<?extendsEmployee>employees=...;// passes successfully 👍for(Employeee:employees){System.out.println(e);}
Emloyee
is a supertype for any potential element the list may contain. No caveats here.
Lower Bounds
What element can we add toList<? super Employee>
? The logic tells us that it's eitherObject
orEmployee
. And it fools us again.
List<?superEmployee>employees=...;employees.add(newAccountant());// passes successfully 👍employees.add(newManager());// passes successfully 👍employees.add(newSoftwareEngineer());// passes successfully 👍employees.add(newEmployee(){/*implementation*/}// passes successfully 👍);employees.add(newObject());// compile error
Again, to figure out this case let’s find out what collections can be assigned toList<? super Employee>
.
List<?superEmployee>employees=newArrayList<Employee>();List<?superEmployee>objects=newArrayList<Object>();
The compiler knows that the list can consist of eitherObject
types orEmployee
ones. That’s whyAccountant
,Manager
,SoftwareEngineer
, andEmployee
can be safely added. They all implementEmployee
interface and inherits fromObject
class. At the same time,Object
cannot be added because it does not implementEmployee
.
On the contrary, reading fromList<? super Employee>
is not so easy.
List<?superEmployee>employees=...;// compile errorfor(Employeee:employees){System.out.println(e);}
The compiler cannot be sure that returned item is ofEmployee
type. Perhaps it is anObject
. That’s why the code does not compile.
Upper-Lower Bounds Conclusion
We can resume that upper bound make a collectionread-only while lower bound make itwrite-only. Does it mean that we can use them as return types in order to restrict client’s access to data manipulation? I wouldn’t recommend to do it.
Upper bound collections are notcompletely read-only because you can still addnull
to them. Lower bound collections are notcompletely write-only because you can still read values as anObject
. I consider that it’s much better to use special containers that shall give the required access to an instance. You can either apply standard JDK utilities likeCollections.unmodifiableList
or use libraries that will do the job (Vavr, for instance).
Upper and lower bound collections act much better as input parameters. You should not mix them with return types.
Recursive Generics
We’ve already mentioned recursive generics in this article. It’s theStream
interface. Let’s take a look again.
interfaceStream<T>extendsBaseStream<T,Stream<T>>{<R>Stream<R>map(Function<?superT,?extendsR>mapper);<R>Stream<R>flatMap(Function<?superT,?extendsStream<?extendsR>>mapper);IntStreamflatMapToInt(Function<?superT,?extendsIntStream>mapper);...}
As you can see,Stream
extends fromBaseStream
that is parameterized withStream
itself. What’s the reason for it? Let’s dive intoBaseStream
to find out.
publicinterfaceBaseStream<T,SextendsBaseStream<T,S>>extendsAutoCloseable{Ssequential();Sparallel();Sunordered();SonClose(RunnablecloseHandler);Iterator<T>iterator();Spliterator<T>spliterator();booleanisParallel();voidclose();}
BaseStream
is a typical example of thefluent API but instead of returning the type itself methods returnS extends BaseStream<T, S>
. Let’s imagine thatBaseStream
was designed without this feature.
publicinterfaceBaseStream<T>extendsAutoCloseable{BaseStream<T>sequential();BaseStream<T>parallel();BaseStream<T>unordered();BaseStream<T>onClose(RunnablecloseHandler);Iterator<T>iterator();Spliterator<T>spliterator();booleanisParallel();voidclose();}
How would it affect the whole Stream API?
List<Employee>employees=...;employees.stream().map(Employee::getSalary).parallel().reduce(0L,(acc,next)->acc+next);// compile error ⛔: cannot find symbol
Thereduce
method belongs toStream
but not toBaseStream
interface. Thereforeparallel
method returnsBaseStream
. So,reduce
cannot be found. This becomes clearer in the schema below.
Recursive generics come in handy in this situation.
This approach allows us to segregate interfaces that leads to better maintainability and readability.
Conclusion
I hope that you’ve learned something new about Java generics. If you have any questions or suggestions, please leave your comments down below. Thanks for reading!
Resources
Top comments(2)
For further actions, you may consider blocking this person and/orreporting abuse