📜 ⬆️ ⬇️

Nontrivial problems with generics and possible solutions.

Hello to all! Any Java programmer who has a little knowledge of Java worked with such a thing as generic. This feature appeared already in the 5th version of Java and today I would like to talk about some nontrivial problems associated with the generalized types that I encountered, as well as why they arise and how they can be solved. This article will also be affected by all (un) favorite Hibernate and Spring.

But I will begin by explaining some of the subtleties of generics that are not always understood by beginners in the Java world. If you are an experienced developer, you can not read the first two points.

1) Why do I need a wildcard, extend, super

Wildcard (?) Is used during substitution into a generic class. It means that it does not matter to us what the parameterized type will be or, if the wildcard is used together with the keywords super and extend, then it is only important for us that the parametrized type be a parent or extend a particular class. I will give a clear example of how this is used in practice.
')
public void foobar(Map<String, Object> ms) { ... } 

If we want to pass a variable of type Map <String, Number> to the method, then nothing will turn out (I will tell you in the next paragraph why), but if the method is declared like this, then we will succeed.

 public void foobar(Map<String, ?> ms) { ... } 

That is, in the second case, we are talking about the fact that it does not matter to us what type the value will lie in the map. But this also imposes its own limitations, now we can only call methods of the Object class for values ​​of the map. If we know that only objects of the Number class can be values ​​in this map, we can rewrite the signature of the method as follows.

 public void foobar(Map<String, ? extends Number> ms) { ... } 

Now for the values ​​of the map, we have methods of the Number class. The question arises, why do we need the keyword super? It says that the parameterized type will be the parent for a particular class, but this does not allow polymorphism to call the method of any class, except the base for all - Object. Again, give an example.

 List<? extends Number> list = new ArrayList<Number>(); List<? extends Number> list = new ArrayList<Integer>(); List<? extends Number> list = new ArrayList<Double>(); 

All three declarations are valid, since both Integer and Double inherit from Number. In any of the three cases, we will be able to get from the list a variable of reference type on Number and call methods of this class. Another thing is that in the first case, this link will contain Number and his heirs, in the second Ineger and his heirs, in the third Double and his heirs. And now, what do you think we can write to the list with such an announcement? If you answered - Number and its any successor, then you were mistaken. The answer is nothing! The reason for this is that a sheet declared in this way can actually be both a Number sheet, and an Integer sheet, Double, and indeed any successor to Number and therefore it is not known what type is stored there and what exactly can be written there. Consider the situation with the super keyword.

 List<? super Integer> list = new ArrayList<Integer>(); List<? super Integer> list = new ArrayList<Number>(); List<? super Integer> list = new ArrayList<Object>(); 

In such a situation, everything is exactly the opposite. Without a type cast, we can add a value from a sheet only to a reference variable of type Object, but the record in the sheet is available for all Integer type heirs.

2)
  List <Number> list = new ArrayList <Integer> (). 
Not? Why?!

Sometimes, especially when you use a third-party library (I often had this with the JasperReports library), it hurts to make such an assignment. And when the compiler refuses to collect it, immediately righteous indignation floats. What is the problem? Why? What is polymorphism ?! After all, it seems, what is the actual problem? The Integer sheet is written to the reference variable on the Number sheet, while the Integer is a direct inheritor of the Number type and all its methods are available to it, therefore there should be no problems when getting an item from the collection. But they arise, and if you carefully read about the super keyword, you should have already understood why.
The essence is as follows - the Number sheet and the Integer sheet are still different objects. The Number sheet implies writing to it a Number (and therefore Double, Float, etc.), which, of course, the Integer sheet should not do.

But as they say, if you really want, then you can that which is impossible!

 List<Number> list = (List)new ArrayList<Integer>(); 

That is, we simply erased the information about the type of the original sheet, having received the so-called class with the “raw” type, which can already be assigned to anything. To make such a trick is not recommended.

3) Generic and Spring (or why you need to wipe the interface!)

Reached the most interesting. This problem is not directly related to generic types, but is caused by the style that they open. Consider a few classes.

 @MappedSuperclass public abstract class BaseLinkEntity<S extends BaseEntity, T extends BaseEntity> extends BaseAuditableEntity { @JoinColumn(name = "SOURCE_ID", nullable = false) @ManyToOne(fetch = FetchType.LAZY) @JsonInclude(JsonInclude.Include.NON_EMPTY) protected S source; @JoinColumn(name = "TARGET_ID", nullable = false) @ManyToOne(fetch = FetchType.LAZY) @JsonInclude(JsonInclude.Include.NON_EMPTY) protected T target; } public interface LinkService<L extends BaseLinkEntity>{ List<L> getAllBySourceId(UUID id); List<L> getAllByTargetId(UUID id); } public abstract class BaseLinkService<L extends BaseLinkEntity> implements LinkReportService<L> { protected BaseLinkRepository<L> linkRepository; @Required public void setLinkRepository(BaseLinkRepository<L> linkRepository) { this.linkRepository = linkRepository; } @Override @Transactional(readOnly = true) public List<L> getAllBySourceId(UUID id) { return linkRepository.findBySourceId(id); } @Override @Transactional(readOnly = true) public List<L> getAllByTargetId(UUID id) { return linkRepository.findByTargetId(id); } } public class CardLinkServiceImpl extends BaseLinkReportService<CardLink> { @Override @Autowired @Qualifier("cardLinkReportRepository") public void setLinkRepository(BaseLinkRepository<CardLink> linkRepository) { super.setLinkRepository(linkRepository); } } public class MyClass{ @Autowired private LinkService<CardLink> cardLinkServiceImpl; } 

The almost standard approach of the spring (except for the intermediate base class) is a bundle of interface-implementation. It was done for the convenience of working with tables in the database, in which entities are linked by many to many connections. And everything seems to be fine. But here it was necessary for me in CardLinkServiceImpl to add a couple of specific methods for these links. In order not to produce intermediate interfaces, I initially added them directly to CardLinkServiceImpl and decided to wyr it right in the right place in class. Bottom line: no bin was found in the container.

A little digging on the Internet the reason for this behavior was found. First of all, Spring 3 does not know how to build classes with generic ones, but Spring 4th version was used in the project. The second reason is that in the spring, proxies are often created for classes that he throws into his container.

Spring uses AOP in many places - aspect-oriented programming. With this approach, some logic is not implemented directly in the method, but is hung on it by aop'a means in the spring. To implement this approach at a low level, a spring in runtime changes the byte-code of the classes, adding the necessary logic to them, and it creates a proxy object, in which information about the original object is erased, but information about its interfaces remains.

In this case, aop is used here to manage transactions (the @Transactional annotation). As a result, I had to make specific methods in a separate interface and wyr already on it.

4) Generic and Hibernate 5.2.1

And now the problem in Hibernate, something similar to that described in the third paragraph, but more looking like a bug. A bit of code:

 public class Card{ @JoinColumn(name = "KIND_ID") @ManyToOne(fetch = FetchType.LAZY) protected DocumentKind documentKind; //  BaseEntity,     Id } public class CardLink extends BaseLinkEntity<Card, Card>{ } @NoRepositoryBean public interface BaseLinkRepository<T extends BaseLinkEntity> extends JpaRepository<T, UUID>, JpaSpecificationExecutor<T> { Page<T> findBySourceId(UUID sourceId, Pageable pageRequest); Page<T> findByTargetId(UUID targetId, Pageable pageRequest); } public interface CardLinkReportRepository extends BaseLinkRepository<CardLink>{ List<CardLink> findByTargetDocumentKindId(UUID documentKindId); } 

Spring will not be able to create the CardLinkReportRepository implementation due to the fact that the hibernate cannot find the documentKind property. In fact, he will look for it in the wrong object. To understand what was happening, I had a long time to debug and poke around in the source code of the hibernate. It can be seen that the opportunity to work with generalized types was laid in it, but it was implemented somehow crookedly. I will try to explain the essence in a few words, but to better understand, you can yourself investigate the MetamodelImpl class (you should pay attention to the buildMetamodel methods (Iterator persistentClasses, Set mappedSuperclasses, SessionFactoryImplementor sessionFactory, boolean ignoreUnsupported) and buildEtyType, which is the same for you a man who you have a man who you are at, you can use the same timeFactory method, you can use the wordFactory, the boolean ignoreUnsupported method, the boolean ignoreUnsupported). AttributeFactory (buildAttribute method (AbstractManagedType ownerType, Property property)).

When building a metamodel, Hibernette first enters all the classes (buildEntity) there, and only after that writes their attributes to the entity (analogy in classes - fields) in the method - buildAttribute. The point is that the entity in the metamodel for the BaseLinkEntity object is created in a single instance and the specific attribute type (generic field) is defined only once in the buildAttribute method. Then, when the JPA repository method looks for the documentKind fields, it searches for a field in the class that was entered when the attribute was built. The type itself is taken from the context with which I no longer had the strength to figure out (where and at what point in time it is created). So it turns out that in the fields of BaseLinkEntity we can search, but in the specific type of generic only for one entity from the entire set in the application.
The most interesting (and not yet clear to me) is that if you rewrite it like this, then everything will work.

 @Query("SELECT link FROM CardLink link WHERE link.target.documentKind.id = ?1") public interface CardLinkReportRepository extends BaseLinkRepository<CardLink>{ List<CardLink> findByTargetDocumentKindId(UUID documentKindId); } 

Source: https://habr.com/ru/post/334512/


All Articles