Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to retrieve a list of objects from generic interface without unchecked assignment?

Tags:

java

generics

I have the following interface:

public interface UserRepository<T extends User> {
    List<T> findAll(UserCriteria userCriteria, PageDetails pageDetails);
    T findByEmail(String email);
}

And its implementation:

@Repository
public class JpaUserRepository implements UserRepository<JpaUser> {
    public List<JpaUser> findAll(UserCriteria userCriteria, PageDetails pageDetails) {
       //implementation
    }

    public JpaUser findByEmail(String email) {
       //implementation
    }
}

Now, when I call:

User user = userRepository.findByEmail(email);

in my service class, everything is fine.

But when I call:

List<User> users = userRepository.findAll(userCriteria, pageDetails);

I get unchecked assignment warning with a reason that userRepository has raw type, so result of findAll is erased. If this is indeed the case, shouldn't findByEmail behave the same? It just doesn't seem very consistent.

How can I eliminate the raw type in this scenario? I've tried few things:

Removing <T extends User> from interface and applying it to method like this:

<U extends User> List<U> findAll(UserCriteria userCriteria, PageDetails pageDetails);

That works fine for the service, but repository implementation now gives warning about unchecked overriding (return type requires unchecked conversion).

I've also tried removing generics from interface and method, while keeping the return list generic:

List<? extends User> findAll(UserCriteria userCriteria, PageDetails pageDetails);

That solves the problem, no warnings, but requires me to write the service like this:

List<? extends User> users = userRepository.findAll(userCriteria, pageDetails);

And it feels a bit clunky (maybe it's just me, so please, let me know if this is acceptable from "good programming" perspective).

Either way, is it possible to get List<User> without raw type warnings, while keeping repository untouched?

Thanks a lot for your time.

EDIT: I am not looking for a way to cast the list.

EDIT 2: Repository declaration:

private final UserRepository userRepository;

Some of you suggested to change that declaration to UserRepository<User> userRepository; and thats successfully removes the warnings but Spring cannot find the bean to autowire this way as JpaUserRepository is UserRepository<JpaUser>. Service layer does not know about repository implementation.

like image 580
Sikor Avatar asked Apr 28 '18 18:04

Sikor


1 Answers

It would help if you showed how userRepository is declared, because that is missing. But the issue is that the repository has the generic type <T extends User>, and this is not the same as <User>, which is why your code is giving warnings.

The question is not so much about the warnings, as it is about the proper usage of generic types. I'm guessing that your userRepository is declared as follows:

@Autowired
private JpaUserRepository userRepository;

But that means that the name is wrong. After all, it's not a repository of User objects, but of JpaUser objects.

Now, you may argue that JpaUser is derived from User. But that's not how Java generics work. There's an excellent resource out there, the Java Generics FAQ. It's really worth reading.

Now I'm going to speculate a bit, in the sense that I think that you're exposing the repository to a user, but you don't want to expose the JpaUser class. But that doesn't make sense, because a repository is a very basic interface that you shouldn't expose in a library.

So, without having all the information, but by doing an educated guess, I can see two scenarios:

  1. You don't expose the repository to a user, in which case you simply handle JpaUser objects.
  2. You do want a user to use User objects, in which case you should build a façade that hides the repository.

Edit: I've quickly made a façade class that you might want to use as a starting point.

public class RepositoryFacade {

    private final UserRepository<? extends User> repository;

    public RepositoryFacade(UserRepository<? extends User> repository) {
        this.repository = repository;
    }

    public List<User> findAll(final UserCriteria userCriteria, final PageDetails pageDetails) {
        return repository.findAll(userCriteria, pageDetails)
                .stream()
                .collect(Collectors.toList());
    }

    public User findByEmail(final String email) {
        return repository.findByEmail(email);
    }
}

public RepositoryFacade getJpaUserFacade() {
    return new RepositoryFacade(new JpaUserRepository());
}

It compiles without any warnings, because the compiler infers the correct types. I'll admit that I find it lacking a certain elegance, but it works.

like image 111
SeverityOne Avatar answered Sep 29 '22 11:09

SeverityOne