Optional#map 可以将输入参数的状态更改为 Function lambda 吗?

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

我有一段 Java 代码,它从 Optional#map

的输入参数中包含的集合中删除一个元素
boolean ret = methodReturnsOptioanl()
                .map(project -> project.getDocIds().remove(docId))
                .orElse(false);

其中 project.getDocIds() return 是一组字符串 ID,并且保证不为空。

我已经测试过它并且有效;如果 Optional 为空或集合中不存在 docId,则 ret 为 false。

但是,Optional#map 可以执行此操作并更改成员集的状态和 return Set#remove 操作的布尔结果吗?

我四处搜索,找不到任何明确的答案。

我会说不,最好的方法是将您的 project 映射到分配给您的 project 对象的 docIds,然后调用终端操作 Stream#orElse。此终端操作应构造一个新的(可变的)List/Collection,然后您可以从中删除 docId.

你的代码将如下所示:

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

然而,更高效的内存解决方案是:

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

这与这样一个事实有关,即 Optional#orElseGetSupplier 仅在 optionalVal 变量为空时才被调用。当您使用 Optional#orElse 时,将始终调用此方法,并且将构造一个空的(可能是不必要的)ArrayList 并将其加载到堆中。这意味着当您的 Optional 不为空时,您将根据需要构造两倍数量的对象,而不是一个。

说明

Stream#map 方法是一个中间操作,这意味着它将 Stream 转换为另一个流。不是这种情况。为此,您可以将 orElse 操作用作终端操作,它会生成 List/Object 作为结果,以便您删除 objectId。

说明内存高效解决方案

Optional#orElseGet 仅在 不存在 值时调用 Supplier。以下测试是 运行 验证这一点:

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();
    }
}

测试输出:

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

但是:这不会有太大的影响,如果这个代码使用的时间很短并且不那么频繁(如方法的使用量),因为这个方法可能会立即超出范围。然而,垃圾收集器还有更多工作要做,这意味着不必要地滥用存储字节。

这样使用map()可以吗?不,因为 map() 表达了将可选元素转换为新类型的意图,而这不是您正在做的。

虽然没有要求 map() 操作没有副作用,并且您的原始代码会做您想要的,但它并没有做人们期待 map()。未来的读者可能需要再看一遍才能了解您的代码在做什么。我会建议更明显的东西:

var project = methodReturnsOptional();
boolean ret = project.isPresent() && project.get().getDocIds().remove(docId);