8

I wanted to filter list of Person class and finally map to some anonymous class in Java using Streams. I am able to do the same thing very easily in C#.

Person class

class Person
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Address { get; set; }
}

Code to map the result in desire format.

 List<Person> lst = new List<Person>();

 lst.Add(new Person() { Name = "Pava", Address = "India", Id = 1 });
 lst.Add(new Person() { Name = "tiwari", Address = "USA", Id = 2 });
 var result = lst.Select(p => new { Address = p.Address, Name = p.Name }).ToList();

Now if I wanted to access any property of newly created type I can easily access by using below mentioned syntax.

Console.WriteLine( result[0].Address);

Ideally I should use loop to iterate over the result.

I know that in java we have collect for ToList and map for Select. But i am unable to select only two property of Person class. How can i do it Java

Pavan Tiwari
  • 3,077
  • 3
  • 31
  • 71
  • I don't know C#'s `Select()` but have a look at Java's `filter()` (of course you'd have to call `stream()` on the list first, then `filter()` on the stream). – Thomas May 15 '17 at 07:37
  • Java doesn't have anonymous types. You just have to write your own class to hold the 2 values. – Dennis_E May 15 '17 at 07:45
  • 1
    Java doesn’t have auto-typed variable declarations of the `var result` kind either. So you have to name the type in the variable declaration, which makes the idea of returning an anonymous type from the `map` operation moot… – Holger May 15 '17 at 15:23
  • Could you please explain how the elements of the `result` list are used? I.e. how is the C# syntax to refer to the fields of the anonymous class? Or is the type of the elements of the result list `Person` after all? – fps May 15 '17 at 17:10
  • @FedericoPeraltaSchaffner Console.WriteLine( result[0].Address); we can use to access Address. – Pavan Tiwari May 16 '17 at 11:15

4 Answers4

14

Java does not have structural types. The closest you could map the values to, are instances of anonymous classes. But there are significant drawbacks. Starting with Java 16, using record would be the better solution, even if it’s a named type and might be slightly more verbose.

E.g. assuming

class Person {
    int id;
    String name, address;

    public Person(String name, String address, int id) {
        this.id = id;
        this.name = name;
        this.address = address;
    }
    public int getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    public String getAddress() {
        return address;
    }
}

you can do

List<Person> lst = List.of(
    new Person("Pava", "India", 1), new Person("tiwari", "USA", 2));
var result = lst.stream()
    .map(p -> {
        record NameAndAddress(String name, String address){}
        return new NameAndAddress(p.getName(), p.getAddress());
    })
    .collect(Collectors.toList());
result.forEach(x -> System.out.println(x.name() + " " + x.address()));

The anonymous inner class alternative would look like

List<Person> lst = List.of(
    new Person("Pava", "India", 1), new Person("tiwari", "USA", 2));
var result = lst.stream()
    .map(p -> new Object(){ String address = p.getAddress(); String name = p.getName();})
    .collect(Collectors.toList());
result.forEach(x -> System.out.println(x.name + " " + x.address));

but as you might note, it’s still not as concise as a structural type. Declaring the result variable using var is the only way to refer to the type we can not refer to by name. This requires Java 10 or newer and is limited to the method’s scope.

It’s also important to keep in mind that inner classes can create memory leaks due to capturing a reference to the surrounding this. In the example, each object also captures the value of p used for its initialization. The record doesn’t have these problems and further, it automatically gets suitable equals, hashCode, and toString implementations, which implies that printing the list like System.out.println(result); or transferring it to a set like new HashSet<>(result) will have meaningful results.

Also, it’s much easier to move the record’s declaration to a broader scope.

Prior to Java 10, lambda expressions are the only Java feature that supports declaring variables of an implied type, which could be anonymous. E.g., the following would work even in Java 8:

List<String> result = lst.stream()
    .map(p -> new Object(){ String address = p.getAddress(); String name = p.getName();})
    .filter(anon -> anon.name.startsWith("ti"))
    .map(anon -> anon.address)
    .collect(Collectors.toList());
Holger
  • 285,553
  • 42
  • 434
  • 765
  • I'm amazed with the last example. I know that types in Java and type inference has been changed to support/improve streams and lambdas, same for the concept of call sites, etc. I'm not aware of the internals of all this, so could you please explain how is it possible for a field of an anonynous class that has been defined in the first call to `map` to be visible in `filter`? I mean, strictly the stream has been transformed from `Stream` to `Stream`, so how is it possible to refer to a field of a type that is anonymous? – fps May 15 '17 at 16:33
  • 2
    @Federico Peralta Schaffner: these are lambda expressions within the same method (or code block), so they are always compiled together and the compiler can infer `Stream` at this point. It’s similar to `new BaseClass() { void member() {} }.member()` which worked long before Java 8. Generally, anonymous types can be used like any other, as long as you get away without referring to them by name. – Holger May 15 '17 at 17:11
  • @FedericoPeraltaSchaffner I was also pretty impressed by this, as it looks very confusing at first. But did it compile for you? I just tried it with an old version of Eclipse (Luna), and it complained about name and address not being resolved, as you would usually expect. Holger, could you elaborate a bit about the background, or do you have a reference somewhere? – Malte Hartwig May 15 '17 at 17:11
  • 1
    @MalteHartwig It compiled fine in IDEA 2017. – fps May 15 '17 at 17:15
  • 2
    @MalteHartwig eclipse compiler != javac. It did not work with eclipse for me also – Eugene May 15 '17 at 17:17
  • @Holger I'm well aware of that `new Object() { void member() {} }.member()` construction. However, in this construction, the method call to `member()` is *chained* (if that's the right term) to the anonymous class declaration, while in the lambdas approach it isn't. I'm glad you have shared this, it's always nice to learn new stuff. – fps May 15 '17 at 17:22
  • Thanks, I guess it really boils down to the compiler. When I tried it with another version, it complained with yet another error... Anyway, thanks for the nice lesson ;) This solution might suffice if @Pavan needs the select only internally, but otherwise, as Holger pointed out, none of the answers really provide a really convincing solution. – Malte Hartwig May 15 '17 at 17:22
  • 1
    @Federico Peralta Schaffner: well, the instance creation and usage are also chained in the `Stream` example, just a bit more complicated. – Holger May 15 '17 at 17:36
  • 2
    @Holger I wonder what people would say if I put this in our code base :). Absolutely magical btw! Had no idea this is possible – Eugene May 15 '17 at 19:05
  • 1
    Updated, to accommodate newer Java versions and to be more specific regarding the drawbacks of the anonymous inner classes. – Holger Sep 03 '21 at 07:39
2

It seems that you want to transform your Person with 3 properties to a Holder that has 2 properties. And that is a simple map operation:

lst.stream().map(p -> new AbstractMap.SimpleEntry(p.address, p.name))
                          .collect(Collectors.toList());

This is collecting your entries to SimpleEntry that is just a Holder for two values. If you need more then two, you are out of luck - you will need to create your own holder(class).

Eugene
  • 117,005
  • 15
  • 201
  • 306
1

If you know which attributes to select and this does not change, I would recommend writing a small class with that subset of Person's attributes. You can then map every person to an instance of that class and collect them into a list:

Stream.of(new Person(1, "a", "aa"), new Person(2, "b", "bb"), new Person(3, "b", "bbb"),
          new Person(4, "c", "aa"), new Person(5, "b", "bbb"))
      .filter(person -> true)    // your filter criteria goes here
      .map(person -> new PersonSelect(person.getName(), person.getAddress()))
      .collect(Collectors.toList());

// result in list of PersonSelects with your name and address

If the set of desired attributes varies, you could use an array instead. It will look more similar to your C# code, but does not provide type safety:

Stream.of(new Person(1, "a", "aa"), new Person(2, "b", "bb"), new Person(3, "b", "bbb"),
          new Person(4, "c", "aa"), new Person(5, "b", "bbb"))
      .filter(person -> true)
      .map(person -> new Object[] {person.getName(), person.getAddress()})
      .collect(Collectors.toList())
      .forEach(p -> System.out.println(Arrays.asList(p)));

// output: [a, aa], [b, bb], [b, bbb], [c, aa], [b, bbb]
Malte Hartwig
  • 4,477
  • 2
  • 14
  • 30
  • In that case result will not be a strongly typed object. – Pavan Tiwari May 16 '17 at 11:18
  • @PavanTiwari That's why I mentioned it in my answer. You will only get an Object[] out of this. I'd recommend the creation of a small class that holds those values. Alternatively, you could use Pair/Tuple classes from frameworks like Guava. Java's API unfortunately does not contain those yet. – Malte Hartwig May 16 '17 at 11:19
0

If you want to create a list of new Person instances you first should provide a constructor, e.g. like this:

class Person {
  public int id;
  public String name;
  public String address;

  public Person( int pId, String pName, String pAddress ) {
    super();
    id = pId;
    name = pName;
    address = pAddress;
  }
}

Then you could use the stream:

List<Person> lst = new ArrayList<>();

lst.add(new Person(1, "Pava", "India" ));
lst.add(new Person( 2, "tiwari", "USA" ) );

//since id is an int we can't use null and thus I used -1 here    
List<Person> result = lst.stream().map(p -> new Person(-1, p.name, p.address)).collect(Collectors.toList());

If you want to filter persons then just put a filter() in between stream() and map():

List<Person> result = lst.stream().filter(p -> p.name.startsWith( "P" )).map(p -> new Person( -1, p.name, p.address )).collect(Collectors.toList());
Thomas
  • 87,414
  • 12
  • 119
  • 157