Vitalii Smahlenko
Vitalii Smahlenko

Reputation: 99

Java method should cache the results

I'm learning programming in the language java.

I need to write an application that takes a string and returns the number of unique characters in the string. It is expected that a string with the same character sequence may be passed several times to the method. Since the counting operation can be time-consuming, the method should cache the results, so that when the method is given a string previously encountered

At this stage, my application is already able to count and display characters

public class Main {
    public static void main(String[] args) {
        String[] testArray = new String[]{"Java", "is", "the", "best", "programming",
                "language", "in", "the", "world!"};
        CharCounter charCounter = new CharCounter();
        Print print = new Print();
        print.printArgs(testArray);
        print.print(charCounter.charCounter(testArray));
    }
}

/**
 *  CharCounter should takes a string and returns the number of unique
 *  characters in the string.
 */
public class CharCounter {
    public LinkedHashMap<Character, Integer> charCounter(String[] args) {
        LinkedHashMap<Character, Integer> elements = new LinkedHashMap();
        List<Character> chars = new ArrayList();
        for (char c : stringToCharArray(args)) {
            chars.add(c);
        }
        for (Character element : chars) {
            if (elements.containsKey(element)) {
                elements.put(element, elements.get(element) + 1);
            } else {
                elements.put(element, 1);
            }
        }
        return elements;
    }

    /**
     * stringToCharArray method - convert string array to character array     *
     */
    private char[] stringToCharArray(String[] args) {
        String s = "";
        for (String agr : args) {
            if (s == "") {
                s = agr;
            } else {
                s = s + " " + agr;
            }
        }
        return s.toCharArray();
    }
}

/**
 * The Print class is intended to output the result to the console
 */
public class Print {
    public void print(Map map) {
        Iterator<Map.Entry<Character, Integer>> iterator
                = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Character, Integer> charCounterEntry = iterator.next();
            System.out.printf("\"%c\" - %d\n", charCounterEntry.getKey(),
                    charCounterEntry.getValue());
        }
    }

    public void printArgs(String[] args) {
        for (String arg : args) {
            System.out.printf("%s ", arg);
        }
        System.out.println();
    }
}

The result of the application

Java is the best programming language in the world! 
"J" - 1
"a" - 5
"v" - 1
" " - 8
"i" - 3
"s" - 2
"t" - 3
"h" - 2
"e" - 4
"b" - 1
"p" - 1
"r" - 3
"o" - 2
"g" - 4
"m" - 2
"n" - 3
"l" - 2
"u" - 1
"w" - 1
"d" - 1
"!" - 1

Now I need to teach my application to cache and check the input data for an already existing result.

I think LoadingCache from Guava will help me

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
   .maximumSize(1000)
   .expireAfterWrite(10, TimeUnit.MINUTES)
   .removalListener(MY_LISTENER)
   .build(
       new CacheLoader<Key, Graph>() {
         @Override
         public Graph load(Key key) throws AnyException {
           return createExpensiveGraph(key);
         }
       });

Please help me pair my app with LoadingCache.

To all who will respond, thanks a lot!

Upvotes: 2

Views: 1500

Answers (1)

xerx593
xerx593

Reputation: 13261

Please help me pair my app with LoadingCache.

Merry X-Mas! There you go:

  1. LoadingCache<Key, Graph> applied to our code must be LoadingCache<String[],Map<Character, Integer>> matching the input and output types of our ...
  2. createExpensiveGraph method applied to our case would be charCounter.
  3. To pair up, we would not invoke charCounter(...) directly, but through a (given) cache instance, so: graphs.get(...).

I refactored "little" (simplified String[] to String, removed "half" of your classes, made the main method interactive), this is what came out:


pom.xml:

<project>
    <!-- only defaults ..., and: -->
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.0.1-jre</version>
        </dependency>
    </dependencies>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
</project>

Main.java

package com.stackoverflow.cache.test;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.LinkedHashMap;
import java.util.Scanner;
import java.util.concurrent.ExecutionException;

public class Main {
  
  static final LoadingCache<String, LinkedHashMap<Character, Integer>> CACHE = CacheBuilder.newBuilder()
      .build( // with defaults, and this loader:
          new CacheLoader<String, LinkedHashMap<Character, Integer>>() {
        @Override
        public LinkedHashMap<Character, Integer> load(String key) {
          System.out.format("Key: \"%s\" not cached.%n", key);
          return analyze(key); // invoking "expensive calculation"! 
        }
      });

  public static void main(String[] args) throws ExecutionException {
    try ( Scanner consoleScanner = new Scanner(System.in)) {
      String word = consoleScanner.nextLine().trim(); // line wise! (for word-wise: next())
      while (!"bye".equalsIgnoreCase(word)) {// from Yoda, greetings! https://blog.codinghorror.com/new-programming-jargon/#1 ;)
        System.out.println(CACHE.get(word));// invoke cache, instead of "expensive" calculation!
        word = consoleScanner.nextLine().trim(); // line wise! (for word-wise: next())
      }
      System.out.println("Bye!");
    }
  }

  // basically your charCounter method with single parameter + stream:
  static LinkedHashMap<Character, Integer> analyze(String arg) {
    LinkedHashMap<Character, Integer> elements = new LinkedHashMap();
    arg.chars().forEach((num) -> {
      Character c = (char) num;
      if (elements.containsKey(c)) {
        elements.put(c, elements.get(c) + 1);
      } else {
        elements.put(c, 1);
      }
    });
    return elements;
  }
}


In- and Output:

>Java is the best programming language in the world!
Key: "Java is the best programming language in the world!" not cached.
{J=1, a=5, v=1,  =8, i=3, s=2, t=3, h=2, e=4, b=1, p=1, r=3, o=2, g=4, m=2, n=3, l=2, u=1, w=1, d=1, !=1}
>hello
Key: "hello" not cached.
{h=1, e=1, l=2, o=1}
>hello
{h=1, e=1, l=2, o=1}
>bye
Bye!

(2nd calculation of "hello" is cached.)


We see: Once identifying/understanding/defining

  • "key"
  • "graph" and
  • "expensive graph operation"

, it is easy to a apply a (guava) cache to a given "operation".

For advanced cache configuration, please refer to CacheBuilder javadoc, for advanced usage to LoadingCache javadoc.

Rather advanced and theoretical but very related to this topic/ use case: Similarity Caching.


To receive "words" from command line arguments, we can use a main() method like this:

public static void main(String[] args) throws ExecutionException {
  for (String word : args) {
    System.out.println(CACHE.get(word)); // ...or custom "print" method
  }
}

To make it completely "without external libs" (i.e. guava)we would (remove/clean up that dependencies,) we would then use it, as outlined in (accepted answer of) Easy, simple to use LRU cache in java :

// limited version:
static final int MAX_ENTRIES = 100;
static final Map<String, Map<Character, Integer>> CACHE = new LinkedHashMap<>(
  MAX_ENTRIES + 1, // initial capacity
  1.0f, // load factor: better 1. than 0.75 (in this setup!?)
  true // "accessOrder" flag
) {
    // eviction: "...This method is invoked by put and putAll after inserting a new entry into the map"
    public boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
};

for "unlimited" cache (might be sufficient for tutor;), just:

// no limit, no "order", no evict, no outdate:
static final Map<String, Map<Character, Integer>> CACHE = new HashMap<>();

(For a thread-safe version, we'd have to:)

 ... CACHE = Collections.synchronizedMap(new xxxMap...);

LinkedHashMap javadoc 17

We could wrap our "cache loading" then like:

static Map<Character, Integer> load(String key) {
  final Map<Character, Integer> result; 
  if (CACHE.containsKey(key)) { // cached!
    result = CACHE.get(key);
    // to "refresh" key, put it again (LRU):
    // CACHE.put(key, result); // here or outside the if-else
  } else { // "expensive" calculation:
    result = analyze(key); // ... and add to cache (unlimited, or removingEldest(imlicitely!!)):
    CACHE.put(key, result);
  }
  return result;
}

and main method like:

public static void main(String[] args) {
  for (String word : args) {
    System.out.println(load(word));
  }
}

;)#

Upvotes: 2

Related Questions