1
\$\begingroup\$

The Stack Overflow question, I'm trying to create an inMemory database using Collections and generics, presents some code that attempts to use Java Generics for an in-memory cache of objects of any particular type that contain an identifier of any particular type.

I revamped that code in to the following code. My code here seems to be working well enough, as seen in some tests below.

Let's ignore the questionable wisdom of writing one’s own cache when many good implementations exist. This code is simply an exercise in trying to master some of the subtleties of Java Generics.

👉 My question is: Have I used Generics properly here? In particular, I do not yet understand where and how to use phrasing such as ? extends T.

I am not asking about the lack of sophistication in the cache API. What I am asking is: Given the limited functionality shown in the current API, are Generics handled correctly and wisely?

First piece of code, the interface for items going into the cache. One cache might hold objects whose identifier is text (String) while another cache might hold objects whose identifier is an integer (Integer), and a third cache for objects whose identifier is a UUID (UUID).

public interface HasId < U > { U getId ( ); } 

Secondly, an example class (record, actually) for objects we might want to store in our cache.

public record User( String username , String name , String surname ) implements HasId < String > { @Override public String getId ( ) { return this.username; } } 

And the cache implementation itself.

👉 Should public class Cache < T extends HasId , U > have a type hanging off the HasId, like this public class Cache < T extends HasId< U > , U >?

import java.util.*; public class Cache < T extends HasId , U > { List < T > entities = new ArrayList <>(); public long size ( ) { return this.entities.size(); } public boolean keep ( T entity ) { return entities.add( entity ); } public boolean drop ( T entity ) { return entities.remove( entity ); } public Optional < T > find ( U id ) { for ( T entity : entities ) { if ( entity.getId().equals( id ) ) { return Optional.of( entity ); } } return Optional.empty(); } List < T > all ( ) { ArrayList < T > list = new ArrayList <>( this.entities ); return List.copyOf( list ); } List < T > allSorted ( Comparator < T > comparator ) { ArrayList < T > list = new ArrayList <>( this.entities ); list.sort( comparator ); return List.copyOf( list ); // Usually best to return an unmodifiable list. } } 

Here are some JUnit 5 Jupiter tests that all pass.

 @Test void addingBobShowsSizeOfOne ( ) { User bob = new User( "bob_barker" , "Bob" , "Barker" ); Cache < User, String > cache = new Cache <>(); cache.keep( bob ); assertEquals( 1 , cache.size() ); } 
 @Test void addingAliceAndBobShowsSizeOfTwo ( ) { User alice = new User( "alice_n" , "Alice" , "Nelson" ); User bob = new User( "bob_b" , "Bob" , "Barker" ); Cache < User, String > cache = new Cache <>(); cache.keep( alice ); cache.keep( bob ); assertEquals( 2 , cache.size() ); } 
 @Test void getAliceAfterAdding ( ) { User alice = new User( "alice_n" , "Alice" , "Nelson" ); Cache < User, String > cache = new Cache <>(); cache.keep( alice ); assertEquals( alice , cache.find( alice.username() ).get() ); } 
 @Test void getAliceAndBobAfterAdding ( ) { User alice = new User( "alice_n" , "Alice" , "Nelson" ); User bob = new User( "bob_b" , "Bob" , "Barker" ); Cache < User, String > cache = new Cache <>(); cache.keep( alice ); cache.keep( bob ); Set < User > users = Set.of( alice , bob ); assertEquals( users , Set.copyOf( cache.all() ) ); } 
 @Test void bobBarkerSortsBeforeAliceNelsonWhenComparingLastName () { User alice = new User( "alice_n" , "Alice" , "Nelson" ); User bob = new User( "bob_b" , "Bob" , "Barker" ); User bogus = new User( "bogus" , "Bogus" , "AAA" ); Cache < User, String > cache = new Cache <>(); cache.keep( alice ); cache.keep( bob ); List<User> expected = List.of(bob , alice ); Comparator<User> c = Comparator.comparing( User::surname ) ; assertEquals( expected , cache.allSorted( c ) ); } 
\$\endgroup\$
2
  • \$\begingroup\$I guess the question is: Do you get compile time errors when you try to do something which would break at runtime, and not get compile time errors when you want to do something sensible?\$\endgroup\$
    – tgdavies
    CommentedMay 23, 2022 at 23:02
  • \$\begingroup\$@tgdavies No compiler errors. But I am not sure of what variations such as subclasses that might cause this cache implementation to fail. If I know what sensible things I might want to do that would cause failure, I would not have posted here today.\$\endgroup\$CommentedMay 23, 2022 at 23:36

1 Answer 1

3
\$\begingroup\$

Yes, you do need to declare Cache as class Cache <T extends HasId<U>, U> otherwise this code compiles:

 Cache<User,Integer> c = new Cache<>(); c.keep(new User("a", "b", "c")); c.find(1); 

As find is comparing Integers and Strings it can never find anything.

With the more explicit declaration the code above gives the error: java: type argument User is not within bounds of type-variable T

\$\endgroup\$

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.