Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Java Generics — Advanced Cases
Semyon Kirekov
Semyon Kirekov

Posted on

     

Java Generics — Advanced Cases

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;}}
Enter fullscreen modeExit fullscreen mode

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);}}
Enter fullscreen modeExit fullscreen mode

Surprisingly this code doesn’t compile.

error: incompatible types: Object cannot be converted to int        for (int num : container.getNumbers()) {                                           ^
Enter fullscreen modeExit fullscreen mode

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);}}
Enter fullscreen modeExit fullscreen mode

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";
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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);}
Enter fullscreen modeExit fullscreen mode

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);
Enter fullscreen modeExit fullscreen mode

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);}
Enter fullscreen modeExit fullscreen mode

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);
Enter fullscreen modeExit fullscreen mode

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);...}
Enter fullscreen modeExit fullscreen mode

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);...}
Enter fullscreen modeExit fullscreen mode

One may notice thatSortingService.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);}
Enter fullscreen modeExit fullscreen mode

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);}
Enter fullscreen modeExit fullscreen mode

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 👍
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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);}
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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>();
Enter fullscreen modeExit fullscreen mode

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);}
Enter fullscreen modeExit fullscreen mode

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);...}
Enter fullscreen modeExit fullscreen mode

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();}
Enter fullscreen modeExit fullscreen mode

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();}
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

Thereduce method belongs toStream but not toBaseStream interface. Thereforeparallel method returnsBaseStream. So,reduce cannot be found. This becomes clearer in the schema below.

image

Recursive generics come in handy in this situation.

image

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
romros profile image
Roman Roshchin
  • Joined

wow! nicely written

CollapseExpand
 
vilgodskiy_sergey profile image
Vilgodskiy Sergey
  • Location
    Nicosia, Cyprus
  • Work
    Java Developer
  • Joined

Nice article! Thank you!
You have small typo:
"The method MoneyService.calculateAvgSalary that we described..."
I think it should be "EmployeeService.calculateAvgSalary"

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Java team lead, conference speaker, and technical author.Telegram for contact: @kirekov
  • Location
    Russia, Moscow
  • Education
    Polzunov Altai State Technical University
  • Work
    Java team lead, a conference speaker, and a lecturer
  • Joined

More fromSemyon Kirekov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp