java - programación - Agrupando por valor de objeto, contando y luego configurando la clave de grupo por atributo de objeto máximo
programacion orientada a objetos ejemplos c++ (4)
Me las arreglé para escribir una solución usando la API de Java 8 Streams que primero agrupa una lista de ruta de objetos por su valor y luego cuenta el número de objetos en cada grupo. Devuelve una ruta de mapeo -> Long. Aquí está el código:
Map<Route, Long> routesCounted = routes.stream()
.collect(Collectors.groupingBy(gr -> gr, Collectors.counting()));
Y la clase de ruta:
public class Route implements Comparable<Route> {
private long lastUpdated;
private Cell startCell;
private Cell endCell;
private int dropOffSize;
public Route(Cell startCell, Cell endCell, long lastUpdated) {
this.startCell = startCell;
this.endCell = endCell;
this.lastUpdated = lastUpdated;
}
public long getLastUpdated() {
return this.lastUpdated;
}
public void setLastUpdated(long lastUpdated) {
this.lastUpdated = lastUpdated;
}
public Cell getStartCell() {
return startCell;
}
public void setStartCell(Cell startCell) {
this.startCell = startCell;
}
public Cell getEndCell() {
return endCell;
}
public void setEndCell(Cell endCell) {
this.endCell = endCell;
}
public int getDropOffSize() {
return this.dropOffSize;
}
public void setDropOffSize(int dropOffSize) {
this.dropOffSize = dropOffSize;
}
@Override
/**
* Compute hash code by using Apache Commons Lang HashCodeBuilder.
*/
public int hashCode() {
return new HashCodeBuilder(43, 59)
.append(this.startCell)
.append(this.endCell)
.toHashCode();
}
@Override
/**
* Compute equals by using Apache Commons Lang EqualsBuilder.
*/
public boolean equals(Object obj) {
if (!(obj instanceof Route))
return false;
if (obj == this)
return true;
Route route = (Route) obj;
return new EqualsBuilder()
.append(this.startCell, route.startCell)
.append(this.endCell, route.endCell)
.isEquals();
}
@Override
public int compareTo(Route route) {
if (this.dropOffSize < route.dropOffSize)
return -1;
else if (this.dropOffSize > route.dropOffSize)
return 1;
else {
// if contains drop off timestamps, order by last timestamp in drop off
// the highest timestamp has preceding
if (this.lastUpdated < route.lastUpdated)
return -1;
else if (this.lastUpdated > route.lastUpdated)
return 1;
else
return 0;
}
}
}
Lo que me gustaría lograr adicionalmente es que la clave para cada grupo sea la que tenga el mayor valor LastUpdated. Ya estaba buscando esta solución, pero no sé cómo combinar el recuento y la agrupación por valor y el valor de Última actualización máxima de ruta. Aquí están los datos de ejemplo de lo que quiero lograr:
EJEMPLO:
List<Route> routes = new ArrayList<>();
routes.add(new Route(new Cell(1, 2), new Cell(2, 1), 1200L));
routes.add(new Route(new Cell(3, 2), new Cell(2, 5), 1800L));
routes.add(new Route(new Cell(1, 2), new Cell(2, 1), 1700L));
DEBE SER CONVERTIDO EN:
Map<Route, Long> routesCounted = new HashMap<>();
routesCounted.put(new Route(new Cell(1, 2), new Cell(2, 1), 1700L), 2);
routesCounted.put(new Route(new Cell(3, 2), new Cell(2, 5), 1800L), 1);
Observe que la clave para el mapeo, que contó 2 Rutas, es la que tiene el mayor valor LastUpdated .
Aquí hay un enfoque. Primero agrupe en listas y luego procese las listas en los valores que realmente desea:
import static java.util.Comparator.comparingLong;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;
Map<Route,Integer> routeCounts = routes.stream()
.collect(groupingBy(x -> x))
.values().stream()
.collect(toMap(
lst -> lst.stream().max(comparingLong(Route::getLastUpdated)).get(),
List::size
));
Cambió igual y hashcode para depender solo de la celda inicial y la celda final.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Cell cell = (Cell) o;
if (a != cell.a) return false;
if (b != cell.b) return false;
return true;
}
@Override
public int hashCode() {
int result = a;
result = 31 * result + b;
return result;
}
Mi solución se ve así:
Map<Route, Long> routesCounted = routes.stream()
.sorted((r1,r2)-> (int)(r2.lastUpdated - r1.lastUpdated))
.collect(Collectors.groupingBy(gr -> gr, Collectors.counting()));
Por supuesto, el envío a int debe reemplazarse con algo más apropiado.
En principio, parece que esto debería ser posible en una sola pasada.
La arruga habitual es que esto requiere una tupla o par ad-hoc, en este caso con una
Route
y un conteo.
Como Java carece de estos, terminamos usando una matriz de objetos de longitud 2 (como se muestra en
la respuesta de Tagir Valeev
), o
AbstractMap.SimpleImmutableEntry
, o una hipotética clase
Pair<A,B>
.
La alternativa es escribir una pequeña clase de valor que contenga una
Route
y un conteo.
Por supuesto, hay algo de dolor en hacer esto, pero en este caso creo que vale la pena porque proporciona un lugar para colocar la lógica de combinación.
Eso a su vez simplifica la operación de transmisión.
Aquí está la clase de valor que contiene una
Route
y un recuento:
class RouteCount {
final Route route;
final long count;
private RouteCount(Route r, long c) {
this.route = r;
count = c;
}
public static RouteCount fromRoute(Route r) {
return new RouteCount(r, 1L);
}
public static RouteCount combine(RouteCount rc1, RouteCount rc2) {
Route recent;
if (rc1.route.getLastUpdated() > rc2.route.getLastUpdated()) {
recent = rc1.route;
} else {
recent = rc2.route;
}
return new RouteCount(recent, rc1.count + rc2.count);
}
}
Bastante sencillo, pero observe el método
combine
.
Combina dos valores de
RouteCount
eligiendo la
Route
que se ha actualizado más recientemente y utilizando la suma de los recuentos.
Ahora que tenemos esta clase de valor, podemos escribir una secuencia de un paso para obtener el resultado que queremos:
Map<Route, RouteCount> counted = routes.stream()
.collect(groupingBy(route -> route,
collectingAndThen(
mapping(RouteCount::fromRoute, reducing(RouteCount::combine)),
Optional::get)));
Al igual que otras respuestas, esto agrupa las rutas en clases de equivalencia basadas en la celda inicial y final.
La instancia de
Route
real utilizada como clave no es significativa;
Es solo un representante de su clase.
El valor será un solo
RouteCount
que contiene la instancia de
Route
que se actualizó más recientemente, junto con el recuento de instancias de
Route
equivalentes.
La forma en que esto funciona es que cada instancia de
Route
que tiene las mismas celdas de inicio y final se alimenta al recopilador aguas abajo de
groupingBy
.
Este recopilador de
mapping
asigna la instancia de
Route
a una instancia de
RouteCount
, luego la pasa a un recopilador
reducing
que reduce las instancias utilizando la lógica de combinación descrita anteriormente.
La porción y luego de
Optional<RouteCount>
extrae el valor del
Optional<RouteCount>
que produce el recopilador
reducing
.
(Normalmente, un
get
desnudo es peligroso, pero no llegamos a este recopilador en absoluto a menos que haya al menos un valor disponible. Así que
get
es seguro en este caso).
Puede definir un método abstracto de "biblioteca" que combine dos recopiladores en uno:
static <T, A1, A2, R1, R2, R> Collector<T, ?, R> pairing(Collector<T, A1, R1> c1,
Collector<T, A2, R2> c2, BiFunction<R1, R2, R> finisher) {
EnumSet<Characteristics> c = EnumSet.noneOf(Characteristics.class);
c.addAll(c1.characteristics());
c.retainAll(c2.characteristics());
c.remove(Characteristics.IDENTITY_FINISH);
return Collector.of(() -> new Object[] {c1.supplier().get(), c2.supplier().get()},
(acc, v) -> {
c1.accumulator().accept((A1)acc[0], v);
c2.accumulator().accept((A2)acc[1], v);
},
(acc1, acc2) -> {
acc1[0] = c1.combiner().apply((A1)acc1[0], (A1)acc2[0]);
acc1[1] = c2.combiner().apply((A2)acc1[1], (A2)acc2[1]);
return acc1;
},
acc -> {
R1 r1 = c1.finisher().apply((A1)acc[0]);
R2 r2 = c2.finisher().apply((A2)acc[1]);
return finisher.apply(r1, r2);
}, c.toArray(new Characteristics[c.size()]));
}
Después de eso, la operación real puede verse así:
Map<Route, Long> result = routes.stream()
.collect(Collectors.groupingBy(Function.identity(),
pairing(Collectors.maxBy(Comparator.comparingLong(Route::getLastUpdated)),
Collectors.counting(),
(route, count) -> new AbstractMap.SimpleEntry<>(route.get(), count))
))
.values().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
Actualización: dicho recopilador está disponible en mi biblioteca
MoreCollectors.pairing()
:
MoreCollectors.pairing()
.
También se implementa un recopilador similar en la biblioteca
jOOL
, por lo que puede usar
Tuple.collectors
lugar de
pairing
.