はじめに
var hogeList = new ArrayList<Hoge>();
のようなオブジェクトのリストに対してソートや重複排除を行う際「どうやるんだっけ?」といつもググってしまうので、自分用にまとめてみる。
ソート
結論:Comparator.comparing().thenComparing()
を使え。
ググるとComparatorクラスを自前で作るだの、ラムダでどうだの出てくるがそれらはJava8くらいの昔話なのでスルーしてよい。
現代Java人はStream APIを使おう。
@Builder
@Getter
@ToString
public class Item {
private int id;
private String name;
}
var items = List.of(
Item.builder().id(5).name("item5").build(),
Item.builder().id(4).name("item4").build(),
Item.builder().id(1).name("item1").build(),
Item.builder().id(3).name("item3").build(),
Item.builder().id(2).name("item2").build()
);
// 昇順ソート
var asc = items.stream()
.sorted(Comparator.comparing(Item::getId))
.toList();
System.out.println(asc.toString());
/**
[Main.Item(id=1, name=item1),
Main.Item(id=2, name=item2),
Main.Item(id=3, name=item3),
Main.Item(id=4, name=item4),
Main.Item(id=5, name=item5)]
**/
// 降順ソート
var desc = items.stream()
.sorted(Comparator.comparing(Item::getId).reversed())
.toList();
System.out.println(desc.toString());
/**
[Main.Item(id=5, name=item5),
Main.Item(id=4, name=item4),
Main.Item(id=3, name=item3),
Main.Item(id=2, name=item2),
Main.Item(id=1, name=item1)]
**/
id, nameの両方でソートすることも可能。thenComparing()
を使う。
var items2 = List.of(
Item.builder().id(1).name("item1-2").build(),
Item.builder().id(1).name("item1-1").build(),
Item.builder().id(1).name("item1-3").build(),
Item.builder().id(3).name("item3-3").build(),
Item.builder().id(3).name("item3-2").build(),
Item.builder().id(3).name("item3-1").build(),
Item.builder().id(2).name("item2-1").build(),
Item.builder().id(2).name("item2-2").build()
);
var sorted = items2.stream()
.sorted(Comparator.comparing(Item::getId).thenComparing(Item::getName))
.toList();
System.out.println(sorted.toString());
/**
[Main.Item(id=1, name=item1-1),
Main.Item(id=1, name=item1-2),
Main.Item(id=1, name=item1-3),
Main.Item(id=2, name=item2-1),
Main.Item(id=2, name=item2-2),
Main.Item(id=3, name=item3-1),
Main.Item(id=3, name=item3-2),
Main.Item(id=3, name=item3-3)]
*/
余談
Integer, Stringなどのリストのソートはもっと単純。
var list = List.of(1, 3, 2, 9, 4, 8, 5, 7, 6);
var sorted = list.stream().sorted().toList();
System.out.println(sorted); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
var reversed = list.stream().sorted(Comparator.reverseOrder()).toList();
System.out.println(reversed); //[9, 8, 7, 6, 5, 4, 3, 2, 1]
重複を排除
方法は色々ある。
以下オブジェクトのリストに対して重複排除する例をいくつか挙げる。
@Builder //lombok
@Getter //lombok
@ToString //lombok
public class Device {
private String name;
private String os;
private String type;
}
Collectors.groupingBy
var devices = List.of(
Device.builder().name("FUJITSU").os("Windows").type("desktop").build(),
Device.builder().name("Dynabook").os("Windows").type("laptop").build(),
Device.builder().name("Surface").os("Windows").type("laptop").build(),
Device.builder().name("MacBook Air").os("Mac").type("laptop").build(),
Device.builder().name("MacBook Pro").os("Mac").type("laptop").build(),
Device.builder().name("iMac").os("Mac").type("desktop").build(),
Device.builder().name("CentOS").os("Linux").type("desktop").build(),
Device.builder().name("Ubuntu").os("Linux").type("desktop").build()
);
// OSで重複排除
var osDistinct = devices.stream()
.collect(Collectors.groupingBy(Device::getOs))
.values().stream().map(device -> device.get(0))
.toList();
osDistinct.forEach(e -> System.out.println(e.getOs()));
/**
Linux
Windows
Mac
**/
//OS, typeの複合キーで重複排除
var osAndTypeDistinct = devices.stream()
.collect(Collectors.groupingBy(device -> StringUtils.join(device.os, "-", device.type)))
.values().stream().map(device -> device.get(0))
.sorted(Comparator.comparing(Device::getOs).thenComparing(Device::getType))
.toList();
osAndTypeDistinct.forEach(device -> System.out.println(StringUtils.join(device.os, "-", device.type)));
/**
Linux-desktop
Mac-desktop
Mac-laptop
Windows-desktop
Windows-laptop
**/
ぱっと見「なにやってんだ?」となるけど、よく読めば大したことはない。
1つ目の// OSで重複排除
の.collect(Collectors.groupingBy(Device::getOs))
でMap<String, List<Device>>
のMapを生成している。
devices.stream().collect(Collectors.groupingBy(Device::getOs))
↓
{Linux=[Main.Device(name=CentOS, os=Linux, type=desktop), Main.Device(name=Ubuntu, os=Linux, type=desktop)],
Windows=[Main.Device(name=FUJITSU, os=Windows, type=desktop), Main.Device(name=Dynabook, os=Windows, type=laptop), Main.Device(name=Surface, os=Windows, type=laptop)],
Mac=[Main.Device(name=MacBook Air, os=Mac, type=laptop), Main.Device(name=MacBook Pro, os=Mac, type=laptop), Main.Device(name=iMac, os=Mac, type=desktop)]}
このMapのvalue(List<Device>
)のそれぞれ一個目の要素だけを引っこ抜いて再びリストに詰めて返す、といった感じ。
Set
var devices = List.of(
Device.builder().name("FUJITSU").os("Windows").type("desktop").build(),
Device.builder().name("Dynabook").os("Windows").type("laptop").build(),
Device.builder().name("Surface").os("Windows").type("laptop").build(),
Device.builder().name("MacBook Air").os("Mac").type("laptop").build(),
Device.builder().name("MacBook Pro").os("Mac").type("laptop").build(),
Device.builder().name("iMac").os("Mac").type("desktop").build(),
Device.builder().name("CentOS").os("Linux").type("desktop").build(),
Device.builder().name("Ubuntu").os("Linux").type("desktop").build()
);
// OSで重複排除
var osSet = new HashSet<String>();
var osDistinct = devices.stream()
.filter(d -> osSet.add(d.getOs()))
.sorted(Comparator.comparing(Device::getOs))
.toList();
osDistinct.forEach(e -> System.out.println(e.getOs()));
/**
Linux
Mac
Windows
**/
HashSetに対象のプロパティを詰めていきながら重複排除を行う。
stream().distinct()
単一キー
@Builder
@Getter
@ToString
public class Device {
private String name;
private String os;
private String type;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Device device = (Device) o;
return Objects.equals(os, device.os);
}
@Override
public int hashCode() {
return Objects.hash(os);
}
}
var devices = List.of(
Device.builder().name("FUJITSU").os("Windows").type("desktop").build(),
Device.builder().name("Dynabook").os("Windows").type("laptop").build(),
Device.builder().name("Surface").os("Windows").type("laptop").build(),
Device.builder().name("MacBook Air").os("Mac").type("laptop").build(),
Device.builder().name("MacBook Pro").os("Mac").type("laptop").build(),
Device.builder().name("iMac").os("Mac").type("desktop").build(),
Device.builder().name("CentOS").os("Linux").type("desktop").build(),
Device.builder().name("Ubuntu").os("Linux").type("desktop").build()
);
// OSで重複排除
var osDistinct = devices.stream()
.distinct()
.sorted(Comparator.comparing(Device::getOs))
.toList();
osDistinct.forEach(e -> System.out.println(e.getOs()));
/**
Linux
Mac
Windows
**/
まずオブジェクトにequalsメソッドを実装する。
(os
の重複排除を行うなら、os
に対するequals)
あとはstream().distinct()を実行するだけでequalsに沿ってos
に対して重複排除が行われる。
複合キー
@Builder
@Getter
@ToString
public class Device {
private String name;
private String os;
private String type;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Device device = (Device) o;
return Objects.equals(os, device.os) && Objects.equals(type, device.type);
}
@Override
public int hashCode() {
return Objects.hash(os, type);
}
}
//OS, typeの複合キーで重複排除
var osAndTypeDistinct = devices.stream()
.distinct()
.sorted(Comparator.comparing(Device::getOs).thenComparing(Device::getType))
.toList();
osAndTypeDistinct.forEach(device -> System.out.println(StringUtils.join(device.os, "-", device.type)));
/**
Linux-desktop
Mac-desktop
Mac-laptop
Windows-desktop
Windows-laptop
**/
equalsをos
, type
の両方に対して実装する。
参考