0
Help us understand the problem. What are the problem?

posted at

updated at

【Java】Stream, Collectorsを使ってオブジェクトのListに対してsort, distinctを行う

はじめに

var hogeList = new ArrayList<Hoge>();のようなオブジェクトのリストに対してソートや重複排除を行う際「どうやるんだっけ?」といつもググってしまうので、自分用にまとめてみる。

ソート

結論:Comparator.comparing().thenComparing()を使え。

ググるとComparatorクラスを自前で作るだの、ラムダでどうだの出てくるがそれらはJava8くらいの昔話なのでスルーしてよい。
現代Java人はStream APIを使おう。

Item.java
@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]

重複を排除

方法は色々ある。
以下オブジェクトのリストに対して重複排除する例をいくつか挙げる。

Device.java
@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()

単一キー

Device.java
@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の両方に対して実装する。

参考

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?