Java 9, Set.of () e Map.of () varargs sobrecargas

Estou estudando os methods de fábrica para collections Immutable . Eu vejo o método Set.of() tem 10 sobrecargas varargs (mesmo para Map.of() ). Eu realmente não consigo entender porque existem tantos. No final, a function ImmutableCollections.SetN(elements) é chamada de qualquer maneira.

Na documentação, encontrei isto:

Embora isso introduza alguma confusão na API, ela evita a alocação de array, a boot e a sobrecarga de garbage collection incorridos pelas chamadas varargs.

A desordem realmente vale o ganho de desempenho? Se sim, isso idealmente criaria um método separado para qualquer elemento N ?

No momento em que o método é chamado de qualquer maneira – isso pode mudar. Por exemplo, pode ser que ele crie um Set com apenas três elementos, 4 e assim por diante.

Também nem todos eles delegam ao SetN – os que têm zero, um e dois elementos têm classs reais de ImmutableCollections.Set0 , ImmutableCollections.Set1 e ImmutableCollections.Set2

Ou você pode ler a pergunta atual sobre isso … aqui Leia os comentários de Stuart Marks nessa questão – como ele é a pessoa que criou essas Coleções.

Alguns aspectos disso podem ser uma forma de revisão futura.

Se você evoluir uma API, precisará prestar atenção em como a assinatura do método será alterada. Por isso, se tivermos

 public class API { public static final  Set of(T... elements) { ... } } 

Poderíamos dizer que o varargs é bom o suficiente … exceto que o varargs força a alocação de um array de objects, que – embora razoavelmente barato – realmente afeta o desempenho. Veja, por exemplo, este microbenchmark, que mostra uma perda de 50% da taxa de transferência para um registro sem operação (ou seja, o nível de log é menor do que o logável) ao alternar para o formato varargs.

Ok, então fazemos algumas análises e dizemos que o caso mais comum é o singleton, então decidimos refatorar …

 public class API { public static final  Set of(T first) { ... } public static final  Set of(T first, T... others) { ... } } 

Ooops … que não é compatível com binário … é compatível com a fonte, mas não compatível com binário … para manter a compatibilidade binária, precisamos manter a assinatura anterior, por exemplo

 public class API { public static final  Set of(T first) { ... } @Deprecated public static final  Set of(T... elements) { ... } public static final  Set of(T first, T... others) { ... } } 

Ugh … O código IDE completo é uma bagunça agora … mais como eu crio um conjunto de arrays? (provavelmente mais relevante se eu estivesse usando uma lista) API.of(new Object[0]) é ambígua … se nós não tivéssemos adicionado o vararg no começo …

Então eu acho que o que eles fizeram foi adicionar argumentos explícitos suficientes para chegar ao ponto em que o tamanho da pilha extra atende ao custo de criação vararg, que é provavelmente cerca de 10 argumentos (pelo menos baseado nas medições que o Log4J2 fez ao adicionar seus varargs à versão 2 API) … mas você está fazendo isso para provas baseadas em evidências …

Em outras palavras, podemos trapacear para todos os casos em que não temos evidências que exijam uma implementação especializada e apenas passar para a variante vararg:

 public class API { private static final  Set internalOf(T... elements) { ... } public static final  Set of(T first) { return internalOf(first); } public static final  Set of(T first, T second) { return internalOf(first, second); } ... public static final  Set of(T t1, T t2, T t3, T t4, T t5, T... rest) { ... } } 

Então podemos traçar um perfil e olhar para os padrões de uso do mundo real e se nós vermos um uso significativo até o formulário de 4 arg e benchmarks mostrando que há um ganho perf razoável, então, nos bastidores, nós mudamos o método impl e todos ganham uma vitória … não é necessária recompilation

Eu acho que depende do escopo da API com a qual você está trabalhando. Ao falar sobre as classs Immutable você está falando sobre coisas incluídas como parte do jdk; então o escopo é muito amplo.

Então você tem:

  1. De um lado, essas classs imutáveis ​​podem ser usadas por aplicativos em que cada bit conta (e cada nanossegundo perdido em alocação / desalocação).
  2. no outro lado, que as aplicações sem essas necessidades não são impactadas negativamente
  3. o único lado ‘negativo’ é para os implementadores dessa API que terão mais problemas para lidar, então isso afeta a manutenção (mas não é uma grande coisa neste caso).

Se você estivesse implementando suas próprias coisas, eu não me importaria muito com isso (mas seja cauteloso com argumentos varargs), a menos que você realmente precise se preocupar com esses bits extras (e performance extra etc.).