Arruinar o desempenho

Esta nota é uma versão escrita do meu relatório "Como arruinar o desempenho com código ineficiente" da conferência JPoint 2018. Você pode assistir a vídeos e slides na página da conferência . No cronograma, o relatório é marcado com um copo ofensivo de smoothies; portanto, não haverá nada super complicado, é mais provável para iniciantes.


Assunto do relatório:


  • como olhar o código para encontrar gargalos nele
  • antipadrões comuns
  • ancinho não óbvio
  • desvio de ancinho

À margem, eles apontaram algumas imprecisões / omissões no relatório, são anotadas aqui. Comentários também são bem-vindos.


Impacto no desempenho


Existe uma classe de usuário:


class User { String name; int age; } 

Como precisamos comparar os objetos, declaramos os métodos equals e hashCode :


 import lombok.EqualsAndHashCode; @EqualsAndHashCode class User { String name; int age; } 

O código é viável, a questão é diferente: o desempenho desse código será o melhor? Para responder, vamos relembrar os recursos do método Object::equals : ele retorna um resultado positivo apenas quando todos os campos comparados são iguais, caso contrário, o resultado será negativo. Em outras palavras, uma diferença já é suficiente para um resultado negativo.


Depois de analisar o código gerado para @EqualsAndHashCode veremos algo assim:


 public boolean equals(Object that) { //... if (name == null && that.name != null) { return false; } if (name != null && !name.equals(that.name)) { return false; } return age == that.age; } 

A ordem de verificação dos campos corresponde à ordem de sua declaração, que no nosso caso não é a melhor solução, porque comparar objetos usando equals "mais difícil" do que comparar tipos simples.


Ok, vamos tentar criar métodos equals/hashCode usando a idéia:


 @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User that = (User) o; return age == that.age && Objects.equals(name, that.name); } 

Uma idéia cria um código mais inteligente que conhece a complexidade da comparação de diferentes tipos de dados. Bem, @EqualsAndHashCode e escreveremos explicitamente equals/hashCode . Agora vamos ver o que acontece quando a classe se estende:


 class User { List<T> props; String name; int age; } 

Recriando equals/hashCode :


 @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User that = (User) o; return age == that.age && Objects.equals(props, that.props) // <---- && Objects.equals(name, that.name); } 

As listas são comparadas antes das cadeias, o que não faz sentido quando as cadeias são diferentes. À primeira vista, não há muita diferença, porque cadeias de comprimento igual são comparadas por sinais (ou seja, o tempo de comparação cresce junto com o comprimento da cadeia):


Houve uma imprecisão

O método java.lang.String::equals é invasivo , portanto, não há comparação de logon na execução.


 //java.lang.String public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { // <---- if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } 

Agora considere comparar duas ArrayList (como a implementação de lista mais usada). Examinando ArrayList , ficamos surpresos ao descobrir que ele não possui sua própria implementação de equals , mas usa uma implementação herdada:



 //AbstractList::equals public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof List)) { return false; } ListIterator<E> e1 = listIterator(); ListIterator<?> e2 = ((List<?>) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { // <---- E o1 = e1.next(); Object o2 = e2.next(); if (!(o1 == null ? o2 == null : o1.equals(o2))) { return false; } } return !(e1.hasNext() || e2.hasNext()); } 

Importante aqui é a criação de dois iteradores e a passagem aos pares através deles. Suponha que haja duas ArrayList :


  • em um número de 1 a 99
  • no segundo número de 1 a 100

Idealmente, seria suficiente comparar os tamanhos das duas listas e, se não coincidirem, retornar imediatamente um resultado negativo (como o AbstractSet ), na realidade, 99 comparações serão realizadas e somente no centésimo ficará claro que as listas são diferentes.


O que há com os kotlinitas?


 data class User(val name: String, val age: Int); 

Aqui tudo é como um Lombok - a ordem de comparação corresponde à ordem do anúncio:


 public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof User) { User u = (User) o; if (Intrinsics.areEqual(name, u.name) && age == u.age) { // <---- return true; } } return false; } 

Como melhorar a situação? - reclama!

IDEA-170178 A comparação de coleções deve ser executada no final de iguais gerados pela IDEA ()
https://youtrack.jetbrains.com/issue/IDEA-170178


Reordenação da comparação no @EqualsAndHashCode para obter melhor desempenho # 1543
https://github.com/rzwitserloot/lombok/issues/1543


KT-23184 Os iguais gerados automaticamente () da classe de dados não são ideais
https://youtrack.jetbrains.com/issue/KT-23184


Como solução alternativa, você pode organizar manualmente as declarações de campo.


Vamos complicar a tarefa


 void check(Dto dto) { SomeEntity entity = jpaRepository.findOne(dto.getId()); boolean valid = dto.isValid(); if (valid && entity.hasGoodRating()) { // <---- //do smth } } 

O código envolve o acesso ao banco de dados, mesmo quando o resultado da verificação das condições indicadas pela seta é previsível com antecedência. Se o valor da variável valid for falso, o código no bloco if nunca será executado, o que significa que você pode fazer sem uma solicitação:


 void check(Dto dto) { boolean valid = dto.isValid(); if (valid && hasGoodRating(dto)) { //do smth } } //       ,    boolean hasGoodRating(Dto dto) { SomeEntity entity = jpaRepository.findOne(dto.getId()); return entity.hasGoodRating(); } 

Nota do lado de fora

O afundamento pode ser insignificante quando a entidade retornada de JpaRepository::findOneJpaRepository::findOne no cache do primeiro nível - então não haverá solicitação.


Um exemplo semelhante sem ramificação explícita:


 boolean checkChild(Dto dto) { Long id = dto.getId(); Entity entity = jpaRepository.findOne(id); return dto.isValid() && entity.hasChild(); } 

Um retorno rápido permite adiar a solicitação:


 boolean checkChild(Dto dto) { if (!dto.isValid()) { return false; } return jpaRepository.findOne(dto.getId()).hasChild(); } 

Uma adição bastante óbvia que não apareceu no relatório

Imagine que uma determinada verificação usa uma entidade semelhante:


 @Entity class ParentEntity { @ManyToOne(fetch = LAZY) @JoinColumn(name = "CHILD_ID") private ChildEntity child; @Enumerated(EnumType.String) private SomeType type; 

Se a verificação usar a mesma entidade, verifique se a chamada para as entidades / coleções filho "preguiçosas" é executada após a chamada para os campos que já estão carregados. À primeira vista, uma solicitação adicional não terá um impacto significativo na imagem geral, mas tudo pode mudar quando uma ação é executada em um loop.


Conclusão: as cadeias de ações / verificações devem ser ordenadas em ordem crescente de complexidade das operações individuais, talvez algumas delas não precisem ser executadas.


Ciclos e processamento em massa


O exemplo a seguir não precisa de explicações especiais:


 @Transactional void enrollStudents(Set<Long> ids) { for (Long id : ids) { Student student = jpaRepository.findOne(id); // <---- O(n) enroll(student); } } 

Devido a várias consultas ao banco de dados, o código é lento.


Observação

O desempenho pode enrollStudents ainda mais se o método enrollStudents executado fora de uma transação: cada chamada para osdjrJpaRepository::findOne será executada em uma nova transação (consulte SimpleJpaRepository ), o que significa receber e retornar uma conexão ao banco de dados, além de criar e liberar o cache de primeiro nível.


Fix:


 @Transactional void enrollStudents(Set<Long> ids) { if (ids.isEmpty()) { return; } for (Student student : jpaRepository.findAll(ids)) { enroll(student); } } 

Vamos medir o tempo de execução (em microssegundos) para uma coleção de chaves (10 e 100 peças)


Referência


Observação

Se você usa Oracle e passa mais de 1000 chaves para findAll , a exceção ORA-01795: maximum number of expressions in a list is 1000 .
Além disso, executar uma consulta pesada (com muitas chaves) in consultas pode ser pior que n consultas. Tudo depende da aplicação específica, portanto a substituição mecânica do ciclo para o processamento em massa pode prejudicar o desempenho.


Um exemplo mais complexo sobre o mesmo tópico


 for (Long id : ids) { Region region = jpaRepository.findOne(id); if (region == null) { // <----  region = new Region(); region.setId(id); } use(region); } 

Nesse caso, não podemos substituir o ciclo por JpaRepository::findAll , JpaRepository::findAll isso quebrará a lógica: todos os valores obtidos de JpaRepository::findAll não serão null e o bloco if não funcionará.


O fato de que para cada chave de banco de dados nos ajudará a resolver essa dificuldade
retorna o valor real ou sua ausência. Ou seja, em certo sentido, um banco de dados é um dicionário. O Java da caixa nos fornece uma implementação pronta do dicionário - HashMap - sobre a qual construiremos a lógica para substituir o banco de dados:


 Map<Long, Region> regionMap = jpaRepository.findAll(ids) .stream() .collect(Collectors.toMap(Region::getId, Function.identity())); for (Long id : ids) { Region region = map.get(id); if (region == null) { region = new Region(); region.setId(id); } use(region); } 

Exemplo reverso


 // class Saver @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(List<AuditEntity> entities) { jpaRepository.save(entities); } 

Esse código sempre cria uma nova transação para salvar uma lista de entidades. A flacidez começa com várias chamadas para um método que abre uma nova transação:


 //   @Transactional public void audit(List<AuditDto> inserts) { inserts.map(this::toEntities).forEach(saver::save); // <---- } // class Saver @Transactional(propagation = Propagation.REQUIRES_NEW) // <---- public void save(List<AuditEntity> entities) { jpaRepository.save(entities); } 

Solução: aplique o método Saver::save imediatamente para todo o conjunto de dados:


 @Transactional public void audit(List<AuditDto> inserts) { List<AuditEntity> bulk = inserts .map(this::toEntities) .flatMap(List::stream) // <---- .collect(toList()); saver.save(bulk); } 

Muitas transações se fundem em uma, o que fornece um aumento tangível (tempo em microssegundos):


Referência


É difícil formalizar um exemplo com várias transações, o que não pode ser dito sobre a chamada de JpaRepository::findOne em um loop.


Portanto, vamos tomar medidas

https://youtrack.jetbrains.com/issue/IDEA-165730
IDEA-165730 Avisar sobre o uso ineficiente do JpaRepository


https://youtrack.jetbrains.com/issue/IDEA-165942
IDEA-165942 Inspeção para substituir a chamada de método em um loop com operação em massa
Corrigido em 2017.1


A abordagem é aplicável não apenas ao banco de dados, então Tagir lany Valeev foi além. E se anteriormente escrevemos assim:


 List<Long> list = new ArrayList<>(); for (Long id : items) { list.add(id); } 

e estava tudo bem, agora a "Idéia" sugere se corrigir:


 List<Long> list = new ArrayList<>(); list.addAll(items); 

Mas mesmo essa opção nem sempre a satisfaz, porque você pode torná-la ainda mais curta e mais rápida:


 List<Long> list = new ArrayList<>(items); 

Compare (tempo em ns)

Para ArrayList, essa melhoria oferece um aumento notável:



Para o HashSet, não é tão otimista:



Referência


Útil para o desenvolvedor:

https://youtrack.jetbrains.com/issue/IDEA-138456
IDEA-138456 Nova inspeção: Collection.addAll () substituível pelo construtor parametrizado
Corrigido em 142.1217


https://youtrack.jetbrains.com/issue/IDEA-178761
IDEA-178761 A inspeção 'Collection.addAll () substituível pelo construtor parametrizado' deve estar ativada por padrão
Corrigido em 2017.3


Removendo do ArrayList


 for (int i = from; i < to; i++) { list.remove(from); } 

O problema está na implementação do método List::remove :


 public E remove(int index) { Objects.checkIndex(index, size); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) { System.arraycopy(array, index + 1, array, index, numMoved); // <---- } array[--size] = null; // clear to let GC do its work return oldValue; } 

Solução:


 list.subList(from, to).clear(); 

Mas e se o valor remoto for usado no código-fonte?


 for (int i = from; i < to; i++) { E removed = list.remove(from); use(removed); } 

Agora você precisa primeiro passar pela lista limpa:


 List<String> removed = list.subList(from, to); removed.forEach(this::use); removed.clear(); 

Se você realmente deseja excluir o ciclo, uma mudança na direção da passagem pela lista ajudará a aliviar a dor. Seu significado é mudar um número menor de elementos após a limpeza da célula:


 //   , . .       for (int i = from; i < to; i++) { E removed = list.remove(from); use(removed, i); } //  , . .    for (int i = to - 1; i >= from; i--) { E removed = list.remove(i); use(removed, reverseIndex(i)); } 

Compare todos os três métodos (nas colunas estão% itens removidos de uma lista de tamanho 100):


Java 8


Java 9


A propósito, alguém notou a anomalia?


Ver


Se excluirmos metade de todos os dados movidos do final, o último elemento será sempre excluído e não haverá mudança:


 // ArrayList public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) { // <----     System.arraycopy(elementData, index+1, elementData, index, numMoved); } elementData[--size] = null; // clear to let GC do its work return oldValue; } 

Referência


Útil para o desenvolvedor

https://youtrack.jetbrains.com/issue/IDEA-177466
IDEA-177466 Detectar List.remove (index) chamado em um loop
Corrigido em 2018.2


Conclusão: operações em massa geralmente são mais rápidas que operações únicas.


Escopo e desempenho


Este código não precisa de explicações especiais:


 void leaveForTheSecondYear() { List<Student> naughty = repository.findNaughty(); List<Student> underAchieving = repository.findUnderAchieving(); // <---- if (settings.leaveBothCategories()) { leaveForTheSecondYear(naughty, underAchieving); // <---- return; } leaveForTheSecondYear(naughty); } 

Limitamos o escopo, o que fornece menos 1 consulta:


 void leaveForTheSecondYear() { List<Student> naughty = repository.findNaughty(); if (Settings.leaveBothCategories()) { List<Student> underAchieving = repository.findUnderAchieving(); // <---- leaveForTheSecondYear(naughty, underAchieving); // <---- return; } leaveForTheSecondYear(naughty); } 

E aqui o leitor atento deve perguntar: e a análise estática? Por que a Idea não nos contou sobre a melhoria que está na superfície?


O fato é que as possibilidades de análise estática são limitadas: se o método é complexo (principalmente interagindo com o banco de dados) e afeta o estado geral, a transferência de sua execução pode interromper o aplicativo. O analisador estático é capaz de relatar execuções muito simples, cuja transferência, digamos, dentro do bloco não quebrará nada.


Você pode usar o destaque variável como uma ersatz, mas novamente, use-o com cuidado, pois os efeitos colaterais são sempre possíveis. Você pode usar a anotação @org.jetbrains.annotations.Contract(pure = true) , disponível na biblioteca jetbrains-annotations para indicar métodos que não mudam de estado:


 // com.intellij.util.ArrayUtil @Contract(pure = true) public static int find(@NotNull int[] src, int obj) { return indexOf(src, obj); } 

Conclusão: na maioria das vezes, o excesso de trabalho só piora o desempenho.


Exemplo mais incomum


 @Service public class RemoteService { private ContractCounter contractCounter; @Transactional(readOnly = true) // <---- public int countContracts(Dto dto) { if (dto.isInvalid()) { return -1; // <---- } return contractCounter.countContracts(dto); } } 

Essa implementação abre uma transação mesmo quando a transação não é necessária (retorno rápido -1 do método).


Tudo que você precisa fazer é remover a transacionalidade dentro do ContractCounter::countContracts , onde for necessário, e removê-la do método "externo".


Compare o tempo de execução para o caso quando -1 (ns) for retornado:


Compare o consumo de memória (bytes):


Referência


Conclusão: controladores e serviços "externos" precisam ser liberados da transacionalidade (essa não é sua responsabilidade) e toda a lógica da verificação de dados de entrada, que não requer acesso ao banco de dados e aos componentes transacionais, deve ser executada lá.


Converter data / hora em string


Uma das tarefas eternas é transformar data / hora em uma sequência. Antes do G8, fizemos o seguinte:


 SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy"); String dateAsStr = formatter.format(date); 

Com o lançamento do JDK 8, obtivemos LocalDate/LocalDateTime e, portanto, DateTimeFormatter


 DateTimeFormatter formatter = ofPattern("dd.MM.yyyy"); String dateAsStr = formatter.format(localDate); 

Vamos medir o seu desempenho:


 Date date = new Date(); LocalDate localDate = LocalDate.now(); SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy"); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd.MM.yyyy"); @Benchmark public String simpleDateFormat() { return sdf.format(date); } @Benchmark public String dateTimeFormatter() { return dtf.format(localDate); } 

Tempo (ns):


Memória (bytes):


Pergunta: digamos que nosso serviço receba dados de fora e não possamos recusar o java.util.Date . Seria vantajoso converter Date em LocalDate se o último for convertido mais rapidamente em uma string? Calcular:


 @Benchmark public String measureDateConverted(Data data) { LocalDate localDate = toLocalDate(data.date); return data.dateTimeFormatter.format(localDate); } private LocalDate toLocalDate(Date date) { return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); } 

Tempo (ns):


Memória (bytes):


Portanto, a conversão Date -> LocalDate benéfica ao usar o "nove". No G8, os custos de conversão absorvem todos os benefícios do DateTimeFormatter -a.


Referência


Conclusão: tire proveito de novas soluções.


Outro "oito"


Neste código, vemos redundância óbvia:


 Iterator<Long> iterator = items // ArrayList<Integer> .stream() .map(Long::valueOf) .collect(toList()) // <----    ? .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

Nós o removemos:


 Iterator<Long> iterator = items // ArrayList<Integer> .stream() .map(Long::valueOf) .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

Vamos ver quanto desempenho melhorou:


Compare com os nove:


Incrível né? Argumentei acima que o excesso de trabalho prejudica o desempenho. Mas aqui removemos o excesso - e (de repente) fica pior. Para entender o que está acontecendo, pegue dois iteradores e olhe para eles sob uma lupa:


Divulgar
 Iterator iterator1 = items.stream().collect(toList()).iterator(); Iterator iterator2 = items.stream().iterator(); 


O primeiro iterador é o ArrayList$Itr regular.


A passagem por ele é simples:
 public boolean hasNext() { return cursor != size; } public E next() { checkForComodification(); int i = cursor; if (i >= size) { throw new NoSuchElementException(); } Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } cursor = i + 1; return (E) elementData[lastRet = i]; } 


O segundo é mais interessante, é o Spliterators$Adapter , que é baseado em ArrayList$ArrayListSpliterator .


Passando por isso é mais difícil
 // java.util.Spliterators$Adapter public boolean hasNext() { if (!valueReady) spliterator.tryAdvance(this); return valueReady; } public T next() { if (!valueReady && !hasNext()) throw new NoSuchElementException(); else { valueReady = false; return nextElement; } } 


Vejamos a iteração do iterador através do async-profiler :


 15.64% juArrayList$ArrayListSpliterator.tryAdvance 10.67% jusSpinedBuffer.clear 9.86% juSpliterators$1Adapter.hasNext 8.81% jusStreamSpliterators$AbstractWrappingSpliterator.fillBuffer 6.01% oojiBlackhole.consume 5.71% jusReferencePipeline$3$1.accept 5.57% jusSpinedBuffer.accept 5.06% cllbir.IteratorFromStreamBenchmark.iteratorFromStream 4.80% jlLong.valueOf 4.53% cllbiIteratorFromStreamBenchmark$$Lambda$8.885721577.apply 

Pode-se observar que a maior parte do tempo é gasta passando pelo iterador, embora em geral não seja necessário, pois a pesquisa pode ser feita assim:


 items .stream() .map(Long::valueOf) .forEach(bh::consume); 

Compare com o resto:


Stream::forEach claramente um vencedor, mas isso é estranho: ainda é baseado no ArrayListSpliterator , mas seu uso melhorou significativamente.


Vamos ver o perfil:
 29.04% oojiBlackhole.consume 22.92% juArrayList$ArrayListSpliterator.forEachRemaining 14.47% jusReferencePipeline$3$1.accept 8.79% jlLong.valueOf 5.37% cllbiIteratorFromStreamBenchmark$$Lambda$9.617691115.accept 4.84% cllbiIteratorFromStreamBenchmark$$Lambda$8.1964917002.apply 4.43% jusForEachOps$ForEachOp$OfRef.accept 4.17% jusSink$ChainedReference.end 1.27% jlInteger.longValue 0.53% jusReferencePipeline.map 

Nesse perfil, a maior parte do tempo é gasta “engolindo” os valores dentro do Blackhole . Comparado a um iterador, uma parte significativamente maior do tempo é gasta diretamente na execução do código Java. Pode-se supor que o motivo seja o menor peso específico da coleta de lixo, comparado com a força bruta do iterador. Verifique:


 forEach:·gc.alloc.rate.norm 100 avgt 30 216,001 ± 0,002 B/op iteratorFromStream:·gc.alloc.rate.norm 100 avgt 30 416,004 ± 0,006 B/op 

De fato, o Stream::forEach fornece metade do consumo de memória.


Por que é mais rápido?

A cadeia de chamadas do começo ao buraco negro é assim:



Como você pode ver, a chamada para ArrayListSpliterator::tryAdvance desapareceu da cadeia e ArrayListSpliterator::forEachRemaining apareceu:


 // ArrayListSpliterator public void forEachRemaining(Consumer<? super E> action) { int i, hi, mc; // hoist accesses and checks from loop ArrayList<E> lst; Object[] a; if (action == null) throw new NullPointerException(); if ((lst = list) != null && (a = lst.elementData) != null) { if ((hi = fence) < 0) { mc = lst.modCount; hi = lst.size; } else mc = expectedModCount; if ((i = index) >= 0 && (index = hi) <= a.length) { for (; i < hi; ++i) { @SuppressWarnings("unchecked") E e = (E) a[i]; // <---- action.accept(e); } if (lst.modCount == mc) return; } } throw new ConcurrentModificationException(); } 

ArrayListSpliterator::forEachRemaining alta velocidade ArrayListSpliterator::forEachRemaining obtido usando uma passagem por toda a matriz em uma chamada de método. Ao usar um iterador, a passagem é limitada a um elemento, portanto, sempre nos ArrayListSpliterator::tryAdvance .
ArrayListSpliterator::forEachRemaining tem acesso a toda a matriz e a ArrayListSpliterator::forEachRemaining com um ciclo de contagem sem chamadas adicionais.


Aviso importante

Observe que a substituição mecânica


 Iterator<Long> iterator = items .stream() .map(Long::valueOf) .collect(toList()) .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

em


 items .stream() .map(Long::valueOf) .forEach(bh::consume); 

Nem sempre é equivalente, porque no primeiro caso usamos uma cópia dos dados para a passagem sem afetar o próprio fluxo e, no segundo caso, os dados são coletados diretamente do fluxo.


Referência


Conclusão: ao lidar com representações complexas de dados, esteja preparado para o fato de que mesmo as regras "de ferro" (trabalho extra prejudicial) param de funcionar. O exemplo acima mostra que a lista intermediária aparentemente supérflua oferece a vantagem de uma implementação mais rápida da enumeração.


Dois truques


 StackTraceElement[] trace = th.getStackTrace(); StackTraceElement[] newTrace = Arrays .asList(trace) .subList(0, depth) .toArray(new StackTraceElement[newDepth]); // <---- 

A primeira coisa que chama sua atenção é uma "melhoria" podre, a saber, passar uma matriz de comprimento diferente de zero para o método Collection::toArray . Explica detalhadamente por que isso é prejudicial.


O segundo problema não é tão óbvio e, para sua compreensão, podemos traçar um paralelo entre o trabalho do revisor e o historiador.


Isto é o que Robin Collingwood escreve sobre isso:

, , [] – . . , , ...


. :


1)
2)
3)


, :


 StackTraceElement[] trace = th.getStackTrace(); StackTraceElement[] newTrace = Arrays.copyOf(trace, depth); //    0 //  StackTraceElement[] newTrace = Arrays.copyOfRange(trace, 0, depth); //   0 

():




 List<T> list = getList(); Set<T> set = getSet(); return list.stream().allMatch(set::contains); //     ? 

, , :


 List<T> list = getList(); Set<T> set = getSet(); return set.containsAll(list); 

():




:


 interface FileNameLoader { String[] loadFileNames(); } 

:


 private FileNameLoader loader; void load() { for (String str : asList(loader.loadFileNames())) { // <----   use(str); } } 

, forEach , :


 private FileNameLoader loader; void load() { for (String str : loader.loadFileNames()) { // <----    use(str); } } 

():



https://youtrack.jetbrains.com/issue/IDEA-182206
IDEA-182206 Simplification for Arrays.asList().subList().toArray()
2018.1


https://youtrack.jetbrains.com/issue/IDEA-180847
IDEA-180847 Inspection 'Call to Collection.toArray with zero-length array argument' brings pessimization
2018.1


https://youtrack.jetbrains.com/issue/IDEA-181928
IDEA-181928 Stream.allMatch(Collection::contains) can be simplified to Collection.containsAll()
2018.1


https://youtrack.jetbrains.com/issue/IDEA-184240
IDEA-184240 Unnecessary array-to-collection wrapping should be detected
2018.1


: :



, , , . , : "" ( ), "" ( ), .



Source: https://habr.com/ru/post/pt423305/


All Articles