группировать по полю в потоках Java - PullRequest
0 голосов
/ 02 февраля 2019

Итак, у меня есть входной JSON, который выглядит так:

[{
  "added": "2014-02-01T09:13:00Z",
  "author": {
    "id": "1",
    "name": "George R R Martin",
    "added_on": "2013-02-01T09:13:00Z"
  },
  "book": {
    "id": "12",
    "name": "Game of Thrones",
    "genre": "Fantasy Fiction"
  }
},
{
  "added": "2015-02-01T09:13:00Z",
  "author": {
    "id": "2",
    "name": "Patrick Rothfuss",
    "added_on": "2012-09-13T011:40:00Z"
  },
  "book": {
    "id": "15",
    "name": "The Name of the Wind",
    "genre": "Fantasy Fiction"
  }
}, {
  "added": "2016-02-01T09:13:00Z",
  "author": {
    "id": "2",
    "name": "Patrick Rothfuss",
    "added_on": "2012-09-13T011:40:00Z"
  },
  "book": {
    "id": "17",
    "name": "The Wise Man's Fear",
    "genre": "Fantasy Fiction"
  }
}]

Мне нужно сгруппировать его на основе author.id.У автора будет один объект и список всех книг, которые он написал.Вот что я ожидаю получить:

[
  {
    "author": "George R R Martin",
    "added_on": "2013-02-01T09:13:00Z",
    "books": [
      {
        "book_name": "Game of Thrones",
        "added": "2014-02-01T09:13:00Z"
      }
    ]
  },
  {
    "author": "Patrick Rothfuss",
    "added_on": "2012-09-13T011:40:00Z",
    "books": [
      {
        "book_name": "The Name of the Wind",
        "added": "2015-02-01T09:13:00Z"
      }, {
        "book_name": "The Wise Man's Fear",
        "added": "2016-02-01T09:13:00Z"
      }
    ]
  }
]

Я попытался сделать это с помощью обычного цикла for - он работает.Но, просто чтобы узнать больше о Streams, я хочу попробовать это с помощью Streams.

Я попробовал это:

Map<Author, List<Book>> collect = authorsList.stream()
                .collect(Collectors.groupingBy(AuthorBookObj::getAuthor,
                        Collectors.mapping(AuthorBookObj::getBook, Collectors.toList())));

Но не получил то, что мне было нужно.Вместо этого он создал три Карты вместо двух.

Также попробовал это:

Map<AuthorTuple, List<Book>> collect = authorsList.stream()
                .collect(Collectors.groupingBy(authors -> new AuthorTuple(authors.getAuthor().getId(),
                                authors.getAuthor().getName(), authors.getAuthor().getAddedOn()),
                        Collectors.mapping(AuthorBookObj::getBook, Collectors.toList())));

Это также дает мне три объекта в списке.Я ожидал иметь двух авторов и соответствующие книги для каждого автора.

AuthBookObj:

public class AuthorBookObj
{

    private String id;

    private Author author;

    private Book book;

    private String added;
    //getter, setter
}

public class Article
{
    private String name;

    private String id;

    private String genre;
}


public class Author
{
    private String name;

    private String added_on;

    private String id;
}

Ответы [ 4 ]

0 голосов
/ 02 февраля 2019

Прежде всего хочу обратить внимание на поле "added" из входного JSON.К чему это относится?Я предполагаю, что это принадлежит Book объекту.Если это так, было бы хорошо поместить это поле (если это возможно) внутри объекта Book.Затем вам нужно десериализовать этот json в java-объекты.Это может быть сделано с помощью com.fasterxml.jackson.databind.ObjectMapper Но для этого вы можете использовать любой json-фреймворк.

ObjectMapper mapper = new ObjectMapper();
AuthorBookObj[] objs = mapper.readValue(inputJson, AuthorBookObj[].class);

Затем вам нужно сгруппировать эти объекты, и ваше первое решение хорошо подходит:

Map<Author, List<Book>> collect = Arrays.stream(objs)
    .collect(groupingBy(AuthorBookObj::getAuthor,
            mapping(AuthorBookObj::getBook, toList())));

Как было упомянуто в предыдущем ответе, вам нужно убедиться, что в вашем классе есть equals/hashcode методы, которые используются в качестве ключа в Map (в данном случае Author).Главное заблуждение теперь в том, что желаемый вывод json не представляет Map.Это просто список некоторых пользовательских объектов с полями, такими как author, added_on, books, который также является списком.

Так что для достижения этой цели вам необходимо преобразовать свой Map<Author, List<Book>> в список пользовательскихобъекты.Например:

public class PublicationInfo {

    private String author;
    private String added_on;
    private List<BookBriefInfo> books;
    ...
}

public class BookBriefInfo {
    private String book_name;
    private String added;
    ...
}


List<PublicationInfo> infos = new ArrayList<>();

for (Map.Entry<Author, List<Book>> entry : collect.entrySet()) {
    PublicationInfo info = new PublicationInfo();
    info.setAuthor(entry.getKey().getName());
    info.setAdded_on(entry.getKey().getAdded_on());
    List<BookBriefInfo> bookInfos = new ArrayList<>();
    for (Book book : entry.getValue()) {
        bookInfos.add(new BookBriefInfo(book.getBook_name(), book.getAdded()))
    }
    info.setBooks(bookInfos);
}

Наконец, его можно сериализовать:

String jsonResult = mapper.writeValueAsString(infos);

Кстати, для форматирования вывода json просто настройте его:

mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
0 голосов
/ 02 февраля 2019

Если у вас нет ограничений на создание новых классов POJO по требованию, я сделаю это следующим образом

Сначала проанализируем входной JSON для объекта java

Класс ответа с AuthorDetails и BookDetails классом

class Response {

private String addedOn;

private AuthorDetails author;

private BookDetails book;
 }

AuthorDetails

class AuthorDetails {
private String id;

private String name;

private String addedOn;
 }

BookDetails

class BookDetails {

private String id;

private String name;

private String gener;
 }

И я сопоставлю входные данные json с List<Response>

List<Response> list = Arrays.asList(new Response());

Затем, преобразовав List<Response> в желаемый вывод, я добавил пару классов POJO

AuthorAndBooks

class AuthorAndBooks {

@JsonProperty("author")
private String author;

@JsonProperty("added_on")
private String addedOn;

@JsonProperty("books")
List<AuthorBooks> books;
 }

AuthorBooks

class AuthorBooks {

@JsonProperty("book_name")
private String name;

@JsonProperty("added")
private String added;

  }

Теперь делайте группы по имени автора

Map<String, List<Response>> group = list.stream().
            collect(Collectors.groupingBy(res->res.getAuthor().getName()));

А теперь для каждого автора добавьтекниги

List<AuthorAndBooks> authorBooks = group.entrySet().stream().
            map(entry->{
                AuthorAndBooks ab = new AuthorAndBooks();
                ab.setAuthor(entry.getKey());
                ab.setAddedOn(entry.getValue().stream().findFirst().get().getAddedOn());

                ab.setBooks(entry.getValue().stream().map(authorBook->{
                    AuthorBooks books = new AuthorBooks();
                    books.setName(authorBook.getBook().getName());
                    books.setAdded(authorBook.getAddedOn());
                    return books;
                }).collect(Collectors.toList()));
    return ab;
            }).collect(Collectors.toList());
0 голосов
/ 02 февраля 2019

Вы должны переопределить equals и hashCode.Если вы этого не сделаете, ваш класс нарушит общий контракт на hashCode, что помешает его правильной работе в коллекциях, таких как HashMap и HashSet.Неспособность класса Author переопределить hashCode приводит к тому, что два равных экземпляра имеют неравные хэш-коды в нарушение договора hashCode.Добавьте это к вашему Author классу.

@Override
public int hashCode() {
    return id.hashCode();
}

@Override
public boolean equals(Object obj) {
    return obj instanceof Author && ((Author) obj).getId().equals(id);
}

С этим на месте следующий фрагмент кода должен работать должным образом.

Map<Author, List<Article>> booksByAuthor = authorsList.stream()
    .collect(Collectors
        .groupingBy(AuthorBookObj::getAuthor, 
            Collectors.mapping(AuthorBookObj::getBook, Collectors.toList())));
0 голосов
/ 02 февраля 2019

Проблема не в том, как вы обрабатываете поток, а в равенстве объектов.

Правильный способ - использовать этот код:

Map<Author, List<Book>> collect = authorsList.stream()
                .collect(Collectors.groupingBy(AuthorBookObj::getAuthor,
                        Collectors.mapping(AuthorBookObj::getBook, Collectors.toList())));

Но теперь вы сравниваете объекты Author, поскольку объекты отличаются, вы получаете три записи.Вам необходимо добавить хеш-код и равно в объекте Author, который будет сравнивать объекты по идентификатору автора.

//code generated from intellij. 
// Author.java
 @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Author author = (Author) o;
        return getId() == author.getId();
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId());
    }
...