Set 的 addAll 抛出 java.lang.UnsupportedOperationException

naz*_*art 1 java exception set unsupportedoperation java-11

使用JDK 11.0.3。我有以下代码片段:

Set<String> allNumbersSet = customerInfoService.getCustomerPhoneNumbers(bankCustomerId);
additionalInformation
        .map(info -> info.get(BANK_PE_CUSTOMER_ID_KEY))
        .filter(StringUtils::isNotEmpty)
        .ifPresent(id -> allNumbersSet.addAll(customerInfoService.getCustomerPhoneNumbers(id))); // fails here
Run Code Online (Sandbox Code Playgroud)

获取电话号码的地方就是Collectors.toSet()

@Override
public Set<String> getCustomerPhoneNumbers(String customerId) {
    return backOfficeInfoClient.getCustByHashNo(customerId).getPropertyLOVs()
            .flatMap(property -> property.getValues().values().stream())
            .collect(Collectors.toSet());
}
Run Code Online (Sandbox Code Playgroud)

但是,它失败了:

java.lang.UnsupportedOperationException
    at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:71)
    at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.addAll(ImmutableCollections.java:76)
    at service.impl.UserManagementServiceImpl.lambda$validateNewLogin$3(UserManagementServiceImpl.java:69)
Run Code Online (Sandbox Code Playgroud)

如果我像下面这样更新:

var allNumbersSet = new HashSet<>(customerInfoService.getCustomerPhoneNumbers(bankCustomerId));
Run Code Online (Sandbox Code Playgroud)

现在效果很好。

上面的代码使用有什么问题吗?您能解释一下为什么会出现这种情况吗?


此方法调用由调用 Hazelcast 缓存包围 - 之前和之后。正如评论中提到的,这可能是这种行为的原因:

缓存的值使用不可变集合表示,这是有道理的,因为这允许共享而无需防御性副本

解决方案:

找到了如何重写此逻辑并在不合并两组的情况下执行该操作的方法:

var numbersSet = customerInfoService.getCustomerPhoneNumbers(id);
if (!numbersSet.contains(newLogin)) {
    var peNumbersSet = additionalInformation
            .map(info -> info.get(BANK_PE_CUSTOMER_ID_KEY))
            .filter(StringUtils::isNotEmpty)
            .map(customerInfoService::getCustomerPhoneNumbers)
            .orElseGet(Collections::emptySet);

    if (!peNumbersSet.contains(newLogin)) {
        throw new ProcessException(ServerError.WRONG_LOGIN_PROVIDED.errorDTO());
    }
}
Run Code Online (Sandbox Code Playgroud)

稍微重新思考一下这个逻辑:

var additionalInformation = Optional.ofNullable(user.getAdditionalInformation());
var phoneNumbers = new HashSet<String>();
additionalInformation
        .map(i -> i.get(BANK_CUSTOMER_ID_KEY))
        .filter(StringUtils::isNotEmpty)
        .map(customerInfoService::getCustomerPhoneNumbers)
        .ifPresent(phoneNumbers::addAll);

additionalInformation
        .map(i -> i.get(BANK_PE_CUSTOMER_ID_KEY))
        .filter(StringUtils::isNotEmpty)
        .map(customerInfoService::getCustomerPhoneNumbers)
        .ifPresent(phoneNumbers::addAll);

if (!phoneNumbers.contains(newLogin)) {
    throw new MetryusProcessException(AuthServerError.WRONG_LOGIN_PROVIDED.errorDTO());
}
Run Code Online (Sandbox Code Playgroud)

然而,了解Collectors.toSet()在不同条件下到底如何工作确实非常有用。

And*_*ner 6

根据JavadocCollectors.toSet()

对于返回的 Set 的类型、可变性、可序列化性或线程安全性没有任何保证。

因此,如果您需要一个可变集,您应该自己创建一个,以确保它是可变的。

您可以使用您拥有的复制构造函数(new HashSet<>(...)- 不要忘记<>)来做到这一点,或者您可以使用:

Collectors.toCollection(HashSet::new)

作为收集器,如链接的 Javadoc 中所述。


但是,请注意,一种更 Stream-y 的方法是连接两个流:

Set<String> someNumbersSet = customerInfoService.getCustomerPhoneNumbers(bankCustomerId);

Set<String> allNumbersSet =
    Stream.concat(
        someNumbersSet.stream(),
        additionalInformation
                .map(info -> info.get(BANK_PE_CUSTOMER_ID_KEY))
                .filter(StringUtils::isNotEmpty)
                .map(customerInfoService::getCustomerPhoneNumbers)
                .stream()
                .flatMap(Collection::stream))
          .collect(Collectors.toSet());
Run Code Online (Sandbox Code Playgroud)

  • @catch23也许,你正在进行一些代码检测,比如注入缓存设施? (3认同)
  • 那么,这可能就是答案。缓存的值使用不可变集合表示,这是有道理的,因为这允许共享而无需防御性副本。 (2认同)