Is it ok for Optional#map to change the state of the input parameter to the Function lambda?

Andy Birchall :

I have a piece of Java code which removes an element from a set contained in the input parameter to Optional#map

boolean ret = methodReturnsOptioanl()
                .map(project -> project.getDocIds().remove(docId))
                .orElse(false);

where project.getDocIds() returns a Set of string ids and is guaranteed to be not null.

I've tested it and works; ret is false if the Optional is empty or docId doesn't exist in the set.

However, is it ok for Optional#map to do this and alter the state of the member set and return the boolean result of the Set#remove operation?

I've searched around and can't find any definitive answer on this.

Remco Buddelmeijer :

I would say no, the best way to do this is to map your project to the docIds assigned to your project Object and to then call the terminal operation Stream#orElse. This terminal operation should construct a new (Mutable) List/Collection, from which you can then remove the docId.

With that your code would look like the following:

boolean ret = optionalVal
              .map(Class::getDocIds)
              .orElse(new ArrayList<>())
              .remove(docId);

A more memory efficient solution would however be:

boolean ret = optionalVal
              .map(Class::getDocIds)
              .orElseGet(ArrayList::new)
              .remove(docId);

This has to do with the fact that the Supplier given to Optional#orElseGet only gets called when the optionalVal variable is empty. When you use Optional#orElse, this method will always be called and with that an empty (and possibly unnecassery) ArrayList will be constructed and loaded into the Heap. This means that when your Optional is not empty, you construct twice as many Objects as needed instead of just one.

Explanation

The Stream#map method is a intermediate operation which means that it transforms the Stream into another stream. This is not the case. For that, you can use the orElse operation as a terminal operation, which produces a List/Object as a result in order for you to remove your objectId.

Explanation memory efficient solution

The Optional#orElseGet only calls the Supplier when the value is not present. The following test was run to verify this:

public class TestTest {

    class TestOptional {

        public TestOptional(){
            System.out.println("TestOptional constructor called.. " + this);
        }

        List<String> getDocIds(){
            System.out.println("TestOptional#getDocIds called.. " + this);
            return new ArrayList<>(Collections.singletonList("test"));
        }

        List<String> getEmptyDocIds(){
            System.out.println("TestOptional#getEmptyDocIds called.. " + this);
            return new ArrayList<>();
        }
    }

    @Test(expected = Exception.class)
    public void test() throws Exception {

        Optional<TestOptional> optionalVal = Optional.of(new TestOptional());
        Optional<TestOptional> optionalValEmpty = Optional.empty();

        boolean deleted = optionalVal
                .map(TestOptional::getDocIds)
                .orElse(new TestOptional().getEmptyDocIds())
                .remove("test");

        System.out.println("One: " + deleted);

        System.out.println("\n ### \n");

        boolean deletedTwo = optionalVal
                .map(TestOptional::getDocIds)
                .orElseGet(() -> new TestOptional().getEmptyDocIds())
                .remove("test");

        System.out.println("Two: " + deletedTwo);

        System.out.println("\n ### \n");

        boolean deletedThree = optionalValEmpty
                .map(TestOptional::getDocIds)
                .orElse(new TestOptional().getEmptyDocIds())
                .remove("test");

        System.out.println("Three: " + deletedThree);

        System.out.println("\n ### \n");

        boolean deletedFour = optionalValEmpty
                .map(TestOptional::getDocIds)
                .orElseGet(() -> new TestOptional().getEmptyDocIds())
                .remove("test");

        System.out.println("Four: " + deletedFour);

        assertThat(deleted).isTrue();
        assertThat(deletedTwo).isTrue();
        assertThat(deletedThree).isFalse();
        assertThat(deletedFour).isFalse();
    }
}

Output of test:

TestOptional constructor called.. test.TestTest$TestOptional@28f67ac7
TestOptional#getDocIds called.. test.TestTest$TestOptional@28f67ac7
TestOptional constructor called.. test.TestTest$TestOptional@1a407d53
TestOptional#getEmptyDocIds called.. test.TestTest$TestOptional@1a407d53
One: true

 ### 

TestOptional#getDocIds called.. test.TestTest$TestOptional@28f67ac7
Two: true

 ### 

TestOptional constructor called.. test.TestTest$TestOptional@3cda1055
TestOptional#getEmptyDocIds called.. test.TestTest$TestOptional@3cda1055
Three: false

 ### 

TestOptional constructor called.. test.TestTest$TestOptional@79b4d0f
TestOptional#getEmptyDocIds called.. test.TestTest$TestOptional@79b4d0f
Four: false

However: This would not have too much of an impact, if this code is used for short time and not so frequently (as in amount of uses of method), as this method probably will be out of scope in no time. It's however still more work for the Garbage Collector which means unnecessary misuse of bytes of storage.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=141911&siteId=1