Теперь это проблема, я хочу сгруппировать поток элементов по другому несвязанному элементу с сопоставимыми атрибутами.
У меня есть класс видов, состоящий из диапазона температур (int lower, upper) и класс Planet, состоящий из имени и постоянной температуры (String name; int temp).
public class Species{
int lower, upper;
constructor....
getter....
}
public class Planet{
String name;
int temp;
contructor....
getter....
}
Как я могу сгруппировать свой Поток планеты по видам, которые могут жить на них (когда температура планеты находится в пределах температурного диапазона вида), что приведет к:
Map <Planet, Set<Species>>
Вот, например, поток планет, которые я хочу сгруппировать:
Set<Species> speciesSet = Stream.of(new Species(5, 70), new Species(100, 220), new Species(75, 80)).collect(Collectors.toSet());
Stream.of(new Planet("blue planet", 45), new Planet("red planet", 150), new Planet("green planet", 77)).collect(Collectors.groupingBy( , Collectors.toSet()));
Моей первой идеей о том, что мне следует сгруппировать, было что-то вроде этого - plnt -> first element from speciesSet.stream() filtered by plnt.getTemp
но это выглядит крайне неэффективно, а также будет проблематично, если на самом деле не будет видов, соответствующих температуре планеты, которые могли бы вернуться в качестве первого элемента.




Для каждой планеты вы определяете, какие виды способны жить, а затем группируете их по планете:
Stream.of(..planets...)
.flatMap(p -> speciesSet.stream().filter(s -> s.lower <= p.temp && p.temp <= s.upper).map(s -> new SimpleEntry<>(p, s)))
.collect(Collectors.groupingBy(Entry::getKey, mapping(Entry::getValue, Collectors.toSet())));
Если ваши планеты уже уникальны, вам даже не нужен groupingBy:
Map<Planet, Set<Species>> result = new HashMap<>();
Stream.of(..planets...)
.forEach(p -> result.put(p,
peciesSet.stream()
.filter(s -> s.lower <= p.temp && p.temp <= s.upper)
.collect(Collectors.toSet()))
Фактически, все, что вам нужно, это поиск, который может сопоставить планету с набором совместимых видов. И это довольно легко сделать, если вы используете класс "сопоставления", например следующий (и это необходимо только потому, что Java не имеет какой-либо структуры данных типа Tuple):
public static class PlanetSpecies {
Planet planet;
Set<Species> species;
public PlanetSpecies(Planet planet, Set<Species> species) {
this.planet = planet;
this.species = species;
}
public Planet getPlanet() {
return planet;
}
public Set<Species> getSpecies() {
return species;
}
}
Используя это, вы можете легко создать коллекцию PlanetSpecies из ваших двух коллекций следующим образом:
Set<PlanetSpecies> planetSpecies = Stream
.of(new Planet("blue planet", 45),
new Planet("red planet", 150),
new Planet("green planet", 77))
.map(speciesMapper)
.collect(Collectors.toSet());
Это приводит к следующему результату, который по сути является вашим исходным списком планет, сопоставленным с набором его совместимых видов:
[ {
"planet" : {
"name" : "blue planet",
"temp" : 45
},
"species" : [ {
"lower" : 5,
"upper" : 70
} ]
}, {
"planet" : {
"name" : "green planet",
"temp" : 77
},
"species" : [ {
"lower" : 75,
"upper" : 80
} ]
}, {
"planet" : {
"name" : "red planet",
"temp" : 150
},
"species" : [ {
"lower" : 100,
"upper" : 220
} ]
} ]
Это в некотором смысле означает, что вам не нужно «группироваться».
Однако, если вам нужен результат, похожий на Map<planet_name, Set<Species>>, необходимо сделать еще кое-что:
Map<String, Set<Species>> planetSpecies = Stream
.of(new Planet("blue planet", 45),
new Planet("red planet", 150),
new Planet("green planet", 77))
.map(speciesMapper)
.collect(
Collectors.groupingBy(
(PlanetSpecies sp) -> sp.getPlanet().getName(),
Collectors.mapping(species -> species.getSpecies(),
Collectors.reducing(new HashSet<Species>(), speciesReducer)
)
));
Редуктор объявлен как:
BinaryOperator<Set<Species>> speciesReducer = (set1, set2) -> {
Set<Species> newSet = new HashSet<>();
newSet.addAll(set1);
newSet.addAll(set2);
return newSet;
};
Этот редуктор в основном выполняет set.union.
Результатом выше будет следующий результат:
{
"green planet" : [ {
"lower" : 75,
"upper" : 80
} ],
"blue planet" : [ {
"lower" : 5,
"upper" : 70
} ],
"red planet" : [ {
"lower" : 100,
"upper" : 220
} ]
}
Во-первых, я бы создал этот метод, чтобы упростить код и улучшить читаемость:
class Planet {
boolean canSupport(Species species) {
return temp >= species.getLower() && temp <= species.getUpper();
}
}
Это также позволяет вам позже легко добавлять дополнительные условия, например требования к атмосфере и требованиям к местным звездам.
Затем сделайте это:
import static // various classes
Map<Planet, Set<Species>> map = Stream.of(...) // Stream of Planets
.distinct() // use this if stream has duplicate Plansts (unlikely)
.map(p -> new SimpleEntry(p, speciesSet.stream().filter(p::canSupport).collect(toSet()))) // collect species for planet
.filter(e -> !e.getValue().isEmpty()) // ignore planets supporting no species
.collect(toMap(Entry::getKey, Entry::getValue));
Полученная карта будет содержать только планеты, которые могут поддерживать как минимум 1 вид, но только один раз рассчитать список видов.
—-
Чтобы повысить эффективность поиска видов, поместите их в два TreeMap<Integer, Set<Species>>, один для верхней и один для более низкой температуры, затем используйте методы NavigableMaps (оставленные читателю), чтобы найти поддерживаемые виды в O (k) (где k - количество поддерживаемых видов) вместо O (n) (количество всех видов).