How to make group operation generic - java

Suppose I have this grouping:
Iterable<WorkExperience> list =
Arrays.asList
(
new WorkExperience(2001, "2001"),
new WorkExperience(2001, "2002"),
new WorkExperience(2001, "2003"),
new WorkExperience(2002, "2004")
);
Stream<WorkExperience> stream = StreamSupport.stream(list.spliterator(), false);
Map<Integer, List<String>> map = stream
.collect(Collectors.groupingBy(WorkExperience::getYear,
Collectors.mapping(WorkExperience::getYearName, Collectors.toList())));
It builds a map which contains work experience objects grouped by year. Works fine. How to make this grouping operation generic?
Ideally I want to do next:
Map<Integer, List<String>> map = new Grouping(list, WorkExperience::getYear, WorkExperience::getYearName).value();
List<Object> list2 = Arrays.asList(new Object(), new Object());
Map<Integer, List<String>> map2 = new Grouping(list2, (obj) -> obj.hashCode, (obj) -> obj.toString).value();

Basically you need to use generics where you use concrete class now.
public static <T, K, V> Map<K, List<V>> groupBy(
Iterable<T> list,
Function<T, K> keyMapper,
Function<T, V> valueMapper) {
Stream<T> stream = StreamSupport.stream(list.spliterator(), false);
return stream
.collect(
Collectors.groupingBy(keyMapper,
Collectors.mapping(valueMapper, Collectors.toList())));
}

This it GroupUtils from one of my project. I use it to group collection by some key field, when only single element for each key, or multiple ones.
public class GroupUtils {
public static <K, V> Map<K, List<V>> groupMultipleBy(Collection<V> data, Function<V, K> classifier) {
return CollectionUtils.isEmpty(data) ? Collections.emptyMap() : data.stream().collect(
Collectors.groupingBy(classifier, Collectors.mapping(Function.identity(), Collectors.toList())));
}
public static <K, V> Map<K, V> groupSingleBy(Collection<V> data, Function<V, K> keyMapper) {
return groupSingleBy(data, keyMapper, Function.identity());
}
public static <K, V, S> Map<K, V> groupSingleBy(Collection<S> data, Function<S, K> keyMapper, Function<S, V> valueMapper) {
return Optional.ofNullable(data).orElse(Collections.emptyList()).stream().collect(Collectors.toMap(keyMapper, valueMapper));
}
}

Related

Sorting Hashmap by values from High to low

I am trying to sort a hashmap that has a structure of by the value from high to Low.
I have created a function below to sort the data.
public static void SortDataHighToLow (Map <String, Integer> UnsortedMap){
List <Integer> list = new ArrayList(UnsortedMap.keySet());
Collections.sort(list,new Comparator <Integer>(){
#Override
public int compare(Integer arg0, Integer arg1) {
return arg0-arg1;
}
});
Map <String, Integer> sortedMap = new LinkedHashMap<>();
for (Integer keys: list){
sortedMap.put(UnsortedMap.toString(), keys);
}
System.out.println(sortedMap);
}
I am recieving the error below:
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
I believe my error is caused by the for() above that I cannot read the Key value.
What adjustment should I make?
Thanks for the help.
Upon the answer came from #deHaar my problem got resolved. The code is below.
private static <K, V> Map<K, V> sortByValue(Map<K, V> map) {
List<Entry<K, V>> list = new LinkedList<>(map.entrySet());
Collections.sort(list, new Comparator<Object>() {
#SuppressWarnings("unchecked")
public int compare(Object o1, Object o2) {
return ((Comparable<V>) ((Map.Entry<K, V>) (o1)).getValue()).compareTo(((Map.Entry<K, V>) (o2)).getValue());
}
});
Map<K, V> result = new LinkedHashMap<>();
for (Iterator<Entry<K, V>> it = list.iterator(); it.hasNext();) {
Map.Entry<K, V> entry = (Map.Entry<K, V>) it.next();
result.put(entry.getKey(), entry.getValue());
}
return result;
}
This is a comparator that do the job:
public class MapKeyByValueComparator<K, T> implements Comparator<K> {
private final Map<K, T> map;
private final Comparator<T> comparator;
public MapKeyByValueComparator(Map<K, T> map, Comparator<T> comparator) {
this.map = map;
this.comparator = comparator;
}
#Override
public int compare(K o1, K o2) {
int ritem = comparator.compare(map.get(o1), map.get(o2));
// CAN NOT RETURNS 0, otherwise key with the same value will be overridden
if (ritem == 0) {
ritem = 1;
}
return ritem;
}
}
And then you can use a TreeMap as:
Map<something, somethig> map = new TreeMap<>(comparator):
map.addAll(...);
But PAY ATTENTION, this brokes the contract of Comparable
It is strongly recommended (though not required) that natural orderings be consistent with equals. This is so because sorted sets (and sorted maps) without explicit comparators behave "strangely" when they are used with elements (or keys) whose natural ordering is inconsistent with equals. In particular, such a sorted set (or sorted map) violates the general contract for set (or map), which is defined in terms of the equals method.

How to create Map<T, List<K>> out of Map<K, List<T> >?

I am trying to implement the function:
private static <T, K> Map<T, List<K> > invertedMap(Map<K, List<T> > m)
For example if I have Map<String, List<Integer> > ,
I want to create another Map<Integer, List<String> >.
I have written some code:
private static <T, K> Map<T, List<K>> invertedMap(Map<K, T> m) {
return m.keySet().stream()
.collect(Collectors.groupingBy(k -> m.get(k)));
}
but as you can see this only works if the map in the argument doesn't contain list as values.
I wouldn't use streams for this (if you want a stream-based solution, check nullpointer's answer):
private static <T, K> Map<T, List<K>> invertedMap(Map<K, List<T>> map) {
Map<T, List<K>> result = new LinkedHashMap<>(); // Preserves insertion order
map.forEach((k, l) ->
l.forEach(t -> result.computeIfAbsent(t, d -> new ArrayList<>()).add(k)));
return result;
}
The above code iterates the input map map and for each element t of each one of its List values l, it uses Map.computeIfAbsent to create the result.
Map.computeIfAbsent returns the value if there's an entry for the given key, or creates the entry and returns the value specified by its second argument d -> new ArrayList<>() (here d stands for a dummy argument that we don't need in order to create a new, empty list). Then, the key k is added to the list returned by Map.computeIfAbsent.
Here is a stream way of doing it (though my first instinct itself would be to follow Federico's solution) :
private static <T, K> Map<T, List<K>> invertedMapOfList(Map<K, List<T>> m) {
return m.entrySet()
.stream()
.flatMap(e -> e.getValue()
.stream()
.map(v -> new AbstractMap.SimpleEntry<>(e.getKey(), v)))
.collect(Collectors.groupingBy(Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())));
}
Hope this will solve your problem.
private static <T, K> Map<T, List<K>> invertedMap(Map<K, List<T>> m) {
Map<T, List<K>> result = new HashMap<T, List<K>>();
for (K key : m.keySet()) {
for (T value : m.get(key)) {
List<K> kList = null;
if ((kList = result.get(value)) == null) {
kList = new ArrayList<K>();
}
kList.add(key);
result.put(value, kList);
}
}
return result;
}
This solution is similar to that suggested in the answer by Federico Peralta Schaffner, except for that it uses for-loops instead of forEach. I'm mainly posting this to have an MCVE and a short example the the input/output, but also as a counterweight for the stream-based solution. Now people can argue about readability and maintainability.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class InvertMapWithLists
{
public static void main(String[] args)
{
Map<String, List<Integer>> map =
new LinkedHashMap<String, List<Integer>>();
map.put("A", Arrays.asList(0,1,2));
map.put("B", Arrays.asList(2,3,4));
map.put("C", Arrays.asList(4,5,6));
System.out.println("Original:");
map.entrySet().forEach(System.out::println);
Map<Integer, List<String>> inverted = invert(map);
System.out.println("Inverted");
inverted.entrySet().forEach(System.out::println);
}
private static <T, K> Map<T, List<K>> invert(
Map<K, ? extends Collection<? extends T>> map)
{
Map<T, List<K>> result = new LinkedHashMap<T, List<K>>();
for (Entry<K, ? extends Collection<? extends T>> entry : map.entrySet())
{
for (T element : entry.getValue())
{
List<K> list = result.computeIfAbsent(
element, v -> new ArrayList<K>());
list.add(entry.getKey());
}
}
return result;
}
}
The output is
Original:
A=[0, 1, 2]
B=[2, 3, 4]
C=[4, 5, 6]
Inverted
0=[A]
1=[A]
2=[A, B]
3=[B]
4=[B, C]
5=[C]
6=[C]

java 8 streams how to convert a Map<String, List<Strings>> to a List<String> will all the string values?

I have a Map<String, List<String>> map and I want extract from it a List<String> that contains the strings of all the list of strings in the map. I'd like to use java8 streams syntax.
In old java I would do:
List<String> all = new LinkedList<String>();
for (String key: map.keySet()) {
all.addAll(map.get(key));
}
return all;
how to do that using streams?
You can do what you want using Stream.flatMap(Function).
public static List<String> collectValues(Map<String, List<String>> map) {
return map.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
A more generic version could look like:
public static <E> List<E> collectValues(Map<?, ? extends Collection<? extends E>> map) {
return map.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
And an even more generic version which allows you to specify the return type:
public static <C extends Collection<E>, E> C collectValues(
Map<?, ? extends Collection<? extends E>> map, Supplier<C> collectionFactory) {
return map.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toCollection(collectionFactory));
}
And finally, just for the fun of it, the most generic version I can think of:
public static <C, E> C collectValues(Map<?, ? extends Iterable<? extends E>> map,
Collector<E, ?, C> collector) {
return map.values().stream()
.flatMap(iterable -> StreamSupport.stream(iterable.spliterator(), false))
.collect(collector);
}
This one uses the StreamSupport class and Collector interface.
Using a new ArrayList and the addAll() method to get the same result.
public class MapTest {
public static void main(String[] args) {
Map<String, List<String>> infoMap = new HashMap<>();
infoMap.put("1", Arrays.asList("a","b","c"));
infoMap.put("2", Arrays.asList("d","e","f"));
infoMap.put("3", Arrays.asList("g","h","i"));
List<String> result = new ArrayList<>();
infoMap.values().stream().forEach(result::addAll);
result.forEach(System.out::println);
}
}

Using Map with TableView in JavaFX

I am trying display a Map in TableView in JavaFX. The code below works just fine but the issue I am having is that any future Map's update after initialize doesn't show in the TableView.
public class TableCassaController<K,V> extends TableView<Map.Entry<K,V>> implements Initializable {
#FXML private TableColumn<K, V> column1;
#FXML private TableColumn<K, V> column2;
// sample data
Map<String, String> map = new HashMap<>();
map.put("one", "One");
map.put("two", "Two");
map.put("three", "Three");
public TableCassaController(ObservableMap<K,V> map, String col1Name, String col2Name) {
System.out.println("Costruttore table");
TableColumn<Map.Entry<K, V>, K> column1 = new TableColumn<>(col1Name);
column1.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Map.Entry<K, V>, K>, ObservableValue<K>>() {
#Override
public ObservableValue<K> call(TableColumn.CellDataFeatures<Map.Entry<K, V>, K> p) {
// this callback returns property for just one cell, you can't use a loop here
// for first column we use key
return new SimpleObjectProperty<K>(p.getValue().getKey());
}
});
TableColumn<Map.Entry<K, V>, V> column2 = new TableColumn<>(col2Name);
column2.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Map.Entry<K, V>, V>, ObservableValue<V>>() {
#Override
public ObservableValue<V> call(TableColumn.CellDataFeatures<Map.Entry<K, V>, V> p) {
// for second column we use value
return new SimpleObjectProperty<V>(p.getValue().getValue());
}
});
ObservableList<Map.Entry<K, V>> items = FXCollections.observableArrayList(map.entrySet());
this.setItems(items);
this.getColumns().setAll(column1, column2);
}
Now, if I try to update the map, this update won't show in the TableView.
use ReadOnlyObjectWrapper
TableColumn<Map.Entry<K, V>, K> column1 = new TableColumn<>(col1Name);
column1.setCellValueFactory(param -> new ReadOnlyObjectWrapper<>(param.getValue().getKey()));
TableColumn<Map.Entry<K, V>, V> column2 = new TableColumn<>(col2Name);
column2.setCellValueFactory(param -> new ReadOnlyObjectWrapper<>(param.getValue().getValue()));

Convert List to Map and filter null keys

Using java 8 streams I want to convert a list into a map like described in the solution to Java 8 List<V> into Map<K, V>. However, I want to filter to remove entries with certain keys (for instance if the key is null) without doing the conversion of the value to a key twice.
For example I could do the filtering prior to collect such as
Map<String, Choice> result =
choices.stream().filter((choice) -> choice.getName() != null).collect(Collectors.toMap(Choice::getName,
Function.<Choice>identity());
In my case the logic to get the key is more complex than simply getting a field property, and I would like to avoid doing the logic first in the filter and again in the keyMapper function of Collectors.toMap
How can I convert the list to a map using a custom keyMapper function and filter certain values based on the new key?
If you want to calculate the key only once, you can use the stream method map to convert the stream to a stream of tuples, filter the tuples based on the key, and finally create the map from the tuples:
Map<String, Choice> result = choices.stream()
.map(c -> new AbstractMap.SimpleEntry<String, Choice>(c.getName(), c))
.filter(e -> e.getKey() != null)
.collect(toMap(e -> e.getKey(), e -> e.getValue()));
Here's a custom collector for what you want:
public class FilteredKeyCollector<T, K, V> implements Collector<T, Map<K, V>, Map<K, V>> {
private final Function<? super T,? extends K> keyMapper;
private final Function<? super T,? extends V> valueMapper;
private final Predicate<K> keyFilter;
private final EnumSet<Collector.Characteristics> characteristics;
private FilteredKeyCollector(Function<? super T,? extends K> keyMapper, Function<? super T,? extends V> valueMapper, Predicate<K> keyFilter) {
this.keyMapper = keyMapper;
this.valueMapper = valueMapper;
this.keyFilter = keyFilter;
this.characteristics = EnumSet.of(Collector.Characteristics.IDENTITY_FINISH);
}
#Override
public Supplier<Map<K, V>> supplier() {
return HashMap<K, V>::new;
}
#Override
public BiConsumer<Map<K, V>, T> accumulator() {
return (map, t) -> {
K key = keyMapper.apply(t);
if (keyFilter.test(key)) {
map.put(key, valueMapper.apply(t));
}
};
}
#Override
public BinaryOperator<Map<K, V>> combiner() {
return (map1, map2) -> {
map1.putAll(map2);
return map1;
};
}
#Override
public Function<Map<K, V>, Map<K, V>> finisher() {
return m -> m;
}
#Override
public Set<Collector.Characteristics> characteristics() {
return characteristics;
}
}
And using it:
Map<String, Choice> result = choices.stream()
.collect(new FilteredKeyCollector<>(
Choice::getName, // key mapper
c -> c, // value mapper
k -> k != null)); // key filter
If you accept doing it in 2 steps you could first collect the map and then remove unwanted keys:
Map<String, Choice> result = choices.stream()
.collect(Collectors.toMap(c -> c.getName(), c -> c);
result.keySet().removeIf(k -> k == null);

Categories

Resources