Java方法参数 – 仅获取不可变列表

huangapple go评论70阅读模式
英文:

Java method parameter - get only Immutable List

问题

我想在我的一个方法中强制使用不可变性。

public record Student (String name, int rank, String school, String district){}

public String topRankingStudentName (List<Student> students) {
    // 我想要这个传入的 students 列表是不可变的
    // 如何强制实现?
}

在这里,我得到 students 作为一个列表。我想强制我的调用者只传递 Immutable 集合。我考虑过获取 ImmutableList,但该类不可访问。使用 JDK-17

怎么做?

英文:

I want to enforce immutability in one of my methods.

public record Student (String name, int rank, String school, String district){}

public String topRankingStudentName (List&lt;Student&gt; students) {
    // I want this incoming students to be Immutable List
    // How to enforce ?
}

Here I am getting the students as a List. I want to enforce to my callers that they pass only Immutable collection. I thought of getting ImmutableList, but that class is not accessible. Using JDK-17

How to do this ?

答案1

得分: 3

Here is the translation of the provided text:

你不能。List API 没有一个通常使用的可访问的不可变子类型(在 Java 中也不能存在这样的东西,最多只能是一个契约协议,没有编译器/运行时强制执行),也没有告诉你的方法。

你有一些选择,但它们都不好。

使用 guava 的 ImmutableList

使用 guava 库中的 ImmutableList 类。请注意,这意味着不可变的非 guava 列表也不会被允许,例如 List.of("a", "b") 是不可变的,但不是 com.google.common.collect.ImmutableList。Guava 在使用 guava 类型在不同模块之间的边界时具有可怕的 API 设计(因为它们不断地破坏向后兼容性,这没问题,但是 guava 没有一个具有仅包含公共 API 中使用的类的单独向后锁定变体,这使得它在这方面非常糟糕)。我强烈建议不要以这种方式做。

编写一个在运行时检查的方法

你可以尝试调用 .set(DUMMY_STUDENT) 然后再次删除该学生(或设置相同的值),如果成功,抛出异常,如果得到 UnsupportedOperationException 则继续执行。这有点巧妙(没有规范保证得到 UOEx 就一定是不可变的)。使用 set 而不是 add,因为 Arrays.asList 支持 set 而不支持 add

public String topRankingStudentName(List<? extends Student> students) {
  ensureImmutable(students);
  ....
}

/**
 * This method relies on the fact that the vast majority
 * of immutable list impls fail-fast when calling
 * mutable methods even if they would have no effect,
 * and that they fail-fast by throwing UnsupportedOperationException.
 */
public static <T> void ensureImmutable(List<T> list) {
  try {
    if (list.size() > 0) {
      list.set(0, list.get(0));
    } else {
      list.clear();
    }
  } catch (UnsupportedOperationException expected) {
    // This is good news - we want this to happen
    return;
  }
  throw new IllegalArgumentException("list should be immutable");
}

我也不会这样做;在某个时候,最好将输入的内容放入文档(javadoc)中,而不是检查所有内容;如果有人有意写破坏的代码,那么无论检查多少次都不会阻止他们。结论:编写超出阻止 API 正常误用的检查并试图阻止故意误用 API 的尝试是不可能成功的,所以不要尝试。

只需在文档中写明

在文档中加入一个说明,表示传递的列表一旦传递后就不能更改,并建议调用者如果无法保证这一点,则使用 List.copyOf(someMutableList)

复制一个副本

你可以始终编写:

public String topRankingStudentName(List<? extends Student> studentsIn) {
  List<? extends Student> students = List.copyOf(studentsIn);
 // 代码在这里
}

如果 studentsIn 是使用 List.of() 创建的,那么在这种情况下,此代码是“智能的”并且不会真正复制(因为 List API 代码知道没有必要这样做)。然而,这并不是你想要的(你想要的是:确保调用者只传递不可变列表),它只是解决了不同的问题(即:你不希望你的代码在运行过程中由于列表在半路中被更改而产生问题)。只有你可以确定这种替代效果在这里是否足够好。

请注意,这还添加了一个要求,提供的列表不包含 null 值。

以不同的方式进行黑客攻击

鉴于 List.copyOf 是“智能的”并且如果是来自核心库的不可变列表则直接返回参数(对于各种不可变列表,例如 Arrays.asList(someZeroLenArray) 或 guava 的 ImmutableList 实例,它不会这样做),你可以使用以下方式:

public String topRankingStudentName(List<? extends Student> students) {
  /* 检查不可变性 */ {
    List<? extends Students> s = List.copyOf(students);
    if (s != students) throw new IllegalArgumentException("students is not immutable");
  }

  // 代码在这里
}

然而,排除各种非基于 List.of 的不可变列表并不是很好。

结尾备注

这些大多数“黑客”都在它们所描述的限制内“有效”,但大多数实际上并没有按照规范进行编程,只是按照当前已知的行为进行编程。我没有特别的理由相信这些行为会发生变化,但如果发生变化,你不能向例如 OpenJDK 团队报告 List.copyOf 的工作方式发生了变化。

因此,出于这个原因和其他一些原因,我强烈建议选择“制作副本”或“只是文档化”的选项。

英文:

You can't. The List API does not have either a generally used accessible immutable subtype (nor could such a thing exist in java, at best, it could be a contract deal, with no compiler/runtime enforcement), nor does it have a method that tells you.

You have a few options, all of them bad.

use guava's ImmutableList

Use the ImmutableList class from the guava library. Note that this means an immutable non-guava list wouldn't be allowed either, e.g. List.of(&quot;a&quot;, &quot;b&quot;) is immutable, but is not a com.google.common.collect.ImmutableList. Guava has terrible API design in regards to using guava types on the boundary between separate modules (due to them breaking backwards compatibility with abandon, which is fine, but guava not having a separate backwards locked variant with just the classes used in public APIs makes it terrible for this). I strongly recommend against doing it this way.

Write a method that checks at runtime

You can attempt to invoke .set(DUMMY_STUDENT) and then remove that student again (or set the same value), throwing an exception if this succeeds, and carrying on if you get an UnsupportedOperationException. This is a bit hacky (there is no spec guarantee that getting an UOEx necessarily means it's immutable). Use set and not add because Arrays.asList supports set and not add:

public String topRankingStudentName(List&lt;? extends Student&gt; students) {
  ensureImmutable(students);
  ....
}

/**
 * This method relies on the fact that the vast majority
 * of immutable list impls fail-fast when calling
 * mutable methods even if they would have no effect,
 * and that they fail-fast by throwing UnsupportedOperationException.
 */
public static &lt;T&gt; void ensureImmutable(List&lt;T&gt; list) {
  try {
    if (list.size() &gt; 0) {
      list.set(0, list.get(0));
    } else {
      list.clear();
    }
  } catch (UnsupportedOperationException expected) {
    // This is good news - we want this to happen
    return;
  }
  throw new IllegalArgumentException(&quot;list should be immutable&quot;);
}

I wouldn't do this either; at some point you're better off putting in the documentation (the javadoc) what the inputs should be instead of checking everything; there are ways around this if someone is intentionally wanting to write broken code. And if someone is intentionally trying to mess it up, no amount of checks are going to stop them. Conclusion: Writing checks that go beyond trying to stop honest misuse of an API and try to stop intentional misuse of an API cannot possibly work, so don't try.

Just write it in the docs

Just put a note in the docs that the list must not be changed once passed, and recommending that callers use List.copyOf(someMutableList) if they can't make this guarantee.

Make a copy

You can always write:

public String topRankingStudentName(List&lt;? extends Student&gt; studentsIn) {
  List&lt;? extends Student&gt; students = List.copyOf(studentsIn);
 // Code here
}

There are situations where this code is 'smart' and doesn't actually make copies - specifically if studentsIn is e.g. made with List.of(), the above makes no copy (because the List API code knows there's no point to doing this). However, this doesn't do what you want (which is: Ensure callers only pass in immutable lists), it merely solves a different problem (which is: You don't want your code to do wonky things because the list is being changed halfway through running). Only you can determine if this alternate effect is good enough here.

Note that this also adds a requirement that the provided list contains no null values.

Hack in a different way

Given that List.copyOf is 'smart' and returns the argument directly if it is an immutable list from the core libraries (it wouldn't do that for various immutable lists, such as Arrays.asList(someZeroLenArray), or guava's ImmutableList instances) - you can use that:

public String topRankingStudentName(List&lt;? extends Student&gt; students) {
  /* Check immutable */ {
    List&lt;? extends Students&gt; s = List.copyOf(students);
    if (s != students) throw new IllegalArgumentException(&quot;students is not immutable&quot;);
  }

  // code here
}

Excluding various non-List.of-based immutable lists with this is not great, though.

Closing notes

Most of these hacks 'work' within the caveats they describe, but most aren't actually programming against spec, just against currently-known behaviour. I have no particular reason to believe these behaviours will ever change, but if they do, you can't file bugs with e.g. team OpenJDK that they changed how List.copyOf works.

For that and a few more reasons I strongly recommend going with either the 'make a copy' or 'just document it' options.

Also note that given the input is (supposed to be) immutable, List&lt;? extends Student&gt; is the right type, not List&lt;Student&gt;. The former lets folks pass List&lt;SomeSpecificStudentSubC;ass&gt; as well, and the only thing that a List&lt;? extends Student&gt; cannot do that a List&lt;Student&gt; can do, is 'add things to it' (doesn't mean the list is immutable, merely that your code cannot add anything except null, it can still remove things though), which, given that you want immutable input, you aren't interested in.

huangapple
  • 本文由 发表于 2023年4月17日 20:44:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/76035291.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定