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" - %dn", 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!
Advertisement
Answer
Please help me pair my app with LoadingCache.
Merry X-Mas! There you go:
LoadingCache<Key, Graph>
applied to our code must beLoadingCache<String[],Map<Character, Integer>>
matching the input and output types of our …createExpensiveGraph
method applied to our case would becharCounter
.- 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...);
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)); } }
;)#