Reputation: 99
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
Reputation: 13261
Please help me pair my app with LoadingCache.
Merry X-Mas! There you go:
LoadingCache<Key, Graph>
applied to our code must be LoadingCache<String[],Map<Character, Integer>>
matching the input and output types of our ...createExpensiveGraph
method applied to our case would be charCounter
.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
, 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));
}
}
;)#
Upvotes: 2