[Resumo das perguntas da entrevista sobre Java] Noções básicas de Java - String+Collection+Generic+IO+Exception+Reflection (edição 2023)

 navegação:

[Notas Dark Horse Java + Resumo de pisar no poço] JavaSE + JavaWeb + SSM + SpringBoot + Riji Takeaway + SpringCloud + Turismo Dark Horse + Guli Mall + Xuecheng Online + Modo Design + Perguntas da entrevista com Nioke

Índice

3. Corda 

3.1.Conjunto de constantes de string

3.2. Fale sobre a classe String

3.3. Quantos objetos string são criados por new String("abc")?

3.4. Qual é a diferença entre String, StringBuffer e Stringbuilder

4. Coleção 

4.1. Conte-nos sobre sua compreensão das coleções Java

4.2. Diga-me a diferença entre Lista e Conjunto

4.3. Fale sobre sua compreensão de ArrayList

4.4. Diga-me a diferença entre ArrayList e LinkedList

4.5. Por favor, fale sobre o princípio subjacente do HashMap

4.6. Fale sobre ConcurrentHashMap

4.7. Diga-me a diferença entre HashMap e Hashtable

5. Genéricos 

5.1. Fale sobre genéricos e apagamento de genéricos

IO 

6.1. Por favor, fale sobre multiplexação IO

6.2. Por favor, fale sobre BIO, NIO, O

6.3. Fale-me sobre Java NIO

7. Anormal  

7.0. Fale sobre o sistema de exceções

7.1. Fale sobre o mecanismo de tratamento de exceções do Java

8. Reflexão 

8.1. Conte-nos sobre sua compreensão da reflexão

Nove, fio


3. Corda 

3.1.Conjunto de constantes de string

Introdução, princípio 

Pool constante: A máquina virtual Java possui um mecanismo de pool constante, que colocará diretamente constantes de string no pool constante para conseguir a reutilização.

Princípio de armazenamento de string Java: 

  1. Ao criar uma constante de string, a JVM verificará se a string existe no conjunto de constantes de string por meio de equals();
  2. Se a string existir no conjunto de constantes de string, a instância de referência será retornada diretamente;
  3. Se não existir, instancie a string primeiro e coloque a referência da string no pool de constantes de string, para que ela possa ser acessada diretamente na próxima vez, obtendo o efeito de uso rápido do cache.

Quando str1 e str2 são atribuídos, eles usam constantes de string. Portanto, str1 e str2 apontam para o mesmo endereço de memória no pool constante, portanto o valor de retorno é verdadeiro.

//比较地址
//只要new,就在堆内存开辟空间。直接赋值字符串在常量池里。
        String str1 = "hello";        //常量池里无“hello”对象,创建“hello”对象,str1指向常量池“hello”对象。
//先检查字符串常量池中有没有"hello",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"hello";
        String str2 = "hello";    //常量池里有“hello”对象,str2直接指向常量池“hello”对象。
        String str3 = new String("hello");   //堆中new创建了一个对象。假如“hello”在常量池中不存在,Jvm还会常量池中创建这个对象“hello”。
        String str4 = new String("hello");
        System.out.println(str1==str2);//true,因为str1和str2指向的是常量池中的同一个内存地址
        System.out.println(str1==str3);//fasle,str1常量池旧地址,str3是new出的新对象,指向一个全新的地址
        System.out.println(str4==str3);//fasle,因为它们引用不同
 
//比较内容
        System.out.println(str4.equals(str3));//true,因为String类的equals方法重写过,比较的是字符串值

3.2. Fale sobre a classe String

marcando pontos

Métodos comuns de String, se String pode ser herdado, duas maneiras de criar string, String imutável e não herdável, aproveitamento do código-fonte

resposta padrão

Métodos comuns de string: 

A classe String contém um grande número de métodos para processar strings, incluindo charAt(), indexOf(), lastIndexOf(), substring(), split(), trim(), toUpperCase(),startWith(), etc.

Duas maneiras de criar strings: 

Existem duas maneiras de criar uma string, uma é usar uma string literal e a outra é usar o construtor new+. Usar o novo método criará mais um objeto e ocupará mais memória, por isso é recomendado usar o método direto para criar strings.

  • Criação literal de string: a JVM usará pool constante para gerenciar esta string;
  • nova criação: A JVM usará primeiro o pool constante para gerenciar a string literal (se a string já existir, ela retornará a referência diretamente, caso contrário, será instanciada e então retornará a referência) e então chamará o construtor de a classe String para criar um novo objeto String, o objeto String recém-criado será salvo na memória heap.

Por que String não pode ser herdada: Como a classe String é modificada por final, a classe String não pode ser herdada.

Por que a string String não pode ser alterada:  Como a matriz de valor do tipo char subjacente da String é modificada finalmente privada, a modificação final faz com que o valor não aponte para a nova matriz (mas não há garantia de que a matriz real apontada para pela variável de referência de valor é imutável), modificação privada e não Expor qualquer método de modificação de valor para o exterior faz com que a matriz real apontada pela variável de referência value seja imutável.

Vantagens imutáveis: como não será alterado, é seguro para threads, economiza espaço e é eficiente.

 Quais padrões de design são úteis no código-fonte String?

Padrão de design Flyweight: quando há um grande número de objetos repetidos em um sistema, se esses objetos repetidos forem objetos imutáveis, você pode usar o padrão Flyweight para projetar os objetos como flyweights e manter apenas uma instância na memória para referência. Isso reduz o número de objetos na memória e, em última análise, economiza memória. O padrão Flyweight é um padrão de design estrutural para criação de objetos.

A diferença entre o modo flyweight e o modo singleton:

  • O padrão singleton está no nível da classe e uma classe só pode ter uma instância de objeto;
  • O modo Flyweight está no nível do objeto. Uma classe pode ter várias instâncias de objetos diferentes, principalmente para resolver o problema de objetos duplicados. A tecnologia de pool geralmente é implementada com o modo Flyweight, como pool de constantes de string, pool de conexões de banco de dados e pool de buffers.
  • O padrão singleton pode ser considerado uma característica especial do padrão flyweight.Quando o padrão flyweight tem apenas uma instância de objeto, é um singleton, e quando há várias instâncias de objeto não repetidas, é um flyweight. O modo de diversão serve principalmente para economizar espaço de memória e melhorar o desempenho do sistema, enquanto o modo singleton serve principalmente para compartilhar dados;

Um literal é um valor fornecido diretamente em um programa por meio do código-fonte .

A classe String é a API mais comumente usada em Java. Ela contém um grande número de métodos para processar strings. Os mais comumente usados ​​são:

  • - char charAt(int index): retorna o caractere no índice especificado;
  • - String substring(int beginIndex, int endIndex): extrai uma parte da substring desta string;
  • - String[] split(String regex): Divida esta string em um array de acordo com as regras especificadas;
  • - String trim(): exclui os espaços iniciais e finais da string;
  • - int indexOf(String str): Retorna o índice da primeira ocorrência da substring nesta string;
  • - int lastIndexOf(String str): Retorna o índice da última ocorrência da substring nesta string;
  • - booleanstartsWith(String prefix): Determina se a string começa com o prefixo especificado;
  • - boolean endsWith (String suffix): Determina se a string termina com o sufixo especificado;
  •  - String toUpperCase(): coloca todos os caracteres desta string em maiúscula;
  •  - String toLowerCase(): todos os caracteres desta string são minúsculos;
  •  - String replaceFirst(String regex, String replacement): substitua a primeira substring correspondente pela string especificada;
  •  - String replaceAll (String regex, String replacement): Substitua todas as substrings correspondentes pela string especificada.

3.3. Quantos objetos string são criados por new String("abc")?

resposta, princípio 

Resposta: um ou dois.

Em primeiro lugar, por causa da palavra-chave new no lado da nova string, um objeto string será definitivamente criado diretamente no heap .

Em segundo lugar, se não houver referência à string "abc" (comparada por iguais) no conjunto de constantes de string, um objeto string será criado no conjunto de constantes de string. Não crie se já existir. Observe que a criação de objetos no pool de constantes de string mencionado aqui, o objeto final ainda é criado no heap, e o pool de constantes de string apenas coloca referências.

3.4. Qual é a diferença entre String, StringBuffer e Stringbuilder

marcando pontos

Seja variável, taxa de reutilização, eficiência, questões de segurança de thread

resposta padrão

String: sequência de caracteres imutável, baixa eficiência, mas alta taxa de reutilização e segurança de thread.

Imutável significa que após a criação do objeto String, a sequência de caracteres no objeto não pode ser alterada até que o objeto seja destruído.

A alta taxa de reutilização significa que após a criação do objeto do tipo String, ele é gerenciado pelo pool de constantes, e o mesmo objeto String pode ser chamado do pool de constantes a qualquer momento. StringBuffer e StringBuider geralmente são chamados após serem convertidos em objetos String após serem criados.

StringBuffer e StringBuilder

Tanto StringBuffer quanto Stringbuilder são strings com sequências de caracteres variáveis, e o método é o mesmo, e eles têm uma classe pai comum, AbstractStringBuilder. 

StringBuffer: sequência de caracteres variável, alta eficiência (adição e exclusão), segurança de thread

StringBuilder: sequência de caracteres variável, o mais eficiente, thread inseguro

Java fornece duas classes String e StringBuffer para encapsular strings e fornece uma série de métodos para manipular objetos string.

String é uma classe imutável, ou seja, após a criação de um objeto String, até que o objeto seja destruído, a sequência de caracteres do objeto não pode ser alterada.

O objeto StringBuffer representa uma string com uma sequência variável de caracteres. Quando um objeto StringBuffer é criado, podemos usar os métodos append(), insert(), reverse(), setCharAt(), setLength() e outros métodos fornecidos por StringBuffer. to Altera a sequência de caracteres deste objeto string. Quando a sequência de caracteres esperada é obtida por meio de StringBuffer, ela pode ser convertida em um objeto String por meio do método toString().

A classe StringBuilder é uma nova classe no JDK1.5 e também representa um objeto string. Comparado com a classe StringBuffer, eles têm uma classe pai comum `AbstractStringBuilder`, ambas basicamente iguais em termos de construtores e métodos. A diferença é que StringBuilder não considera problemas de segurança de thread e, por causa disso, StringBuilder tem um pouco melhor desempenho do que StringBuffer alto . Portanto, se uma grande quantidade de dados for operada em um único thread, a classe StringBuilder deverá ser usada primeiro; se uma grande quantidade de dados for operada em multithreading, a classe StringBuilder deverá ser usada primeiro.

4. Coleção 

4.1. Conte-nos sobre sua compreensão das coleções Java

marcando pontos

Definir, listar, Quque, mapa, interface de coleção, coleção segura para threads

resposta padrão

As classes de coleção em Java são divididas em 4 categorias, representadas por 4 interfaces respectivamente, são elas: Set, List, Queue, Map. Entre elas, as interfaces Set, List e Queue herdam da interface Collection, e a interface Map não herda de outras interfaces.

Conjunto representa uma coleção de elementos não ordenada e não repetível .

Lista representa uma coleção ordenada de elementos repetíveis. Ordenado significa que a ordem dos elementos é determinada diretamente pela ordem de inserção.

Fila significa fila primeiro a entrar, primeiro a sair (FIFO).

Mapa representa uma coleção com um relacionamento de mapeamento (valor-chave).

Java fornece muitas classes de implementação de coleção, que são classes de implementação direta ou indireta dessas interfaces, entre as quais as mais utilizadas são: HashSet, TreeSet, ArrayList, LinkedList, ArrayDeque, HashMap, TreeMap, etc. Essas coleções não são thread-safe.

Coleções thread-safe:

  1. Classe de ferramenta de coleções: O métodosyncedXxx() da classe de ferramenta de coleções agrupa classes de coleção, como ArrayList, em classes de coleção seguras para threads. Por exemplo List<String> sincronizadoList = Collections.synchronizedList(list);
  2. APIs antigas: APIs antigas com baixo desempenho no pacote java.util, como Vector e Hashtable, que apareceram no JDK1 e não são recomendadas porque a solução thread-safe é imatura e o desempenho é ruim.
  3. Contêineres simultâneos com granularidade de bloqueio reduzida: contêineres começando com Concurrent no pacote JUC para reduzir a granularidade de bloqueio e melhorar o desempenho de simultaneidade, como ConcurrentHashMap. É adequado para cenários onde as operações de leitura e gravação são frequentes.
  4. Contêineres simultâneos implementados por tecnologia de cópia: Contêineres simultâneos começando com CopyOnWrite no pacote JUC e implementados por tecnologia de cópia na gravação, como CopyOnWriteArrayList. Ao escrever, primeiro copie o array atual, opere no array copiado e, em seguida, aponte a referência do array original para o array copiado após a conclusão da operação. Evita problemas de segurança de thread de modificação simultânea da mesma matriz. É adequado para cenários onde as operações de leitura são mais frequentes do que as operações de gravação e a quantidade de dados não é grande. É adequado para cenários onde há muito mais operações de leitura do que operações de gravação.
List list = Collections.synchronizedList(new ArrayList());

As interfaces ou implementações das classes de coleção mencionadas acima estão todas localizadas no pacote java.util e a maioria dessas implementações não são seguras para threads . Embora não sejam thread-safe, essas classes apresentam melhor desempenho. Se você precisar usar classes de coleção thread-safe , você pode usar a classe de ferramenta Collections.O métodosyncedXxx() fornecido pela classe de ferramenta pode agrupar essas classes de coleção em classes de coleção thread-safe.

Entre as classes de coleção do pacote java.util, também existem algumas classes de coleção thread-safe, como Vector e Hashtable, que são APIs muito antigas. Embora sejam thread-safe, seu desempenho é ruim e seu uso foi descontinuado.

A partir do JDK1.5, um grande número de contêineres simultâneos eficientes foram adicionados ao pacote simultâneo, e esses contêineres podem ser divididos em três categorias de acordo com o mecanismo de implementação.

A primeira categoria é reduzir a granularidade do bloqueio para melhorar o desempenho simultâneo do contêiner, e seus nomes de classe começam com Concurrent, como ConcurrentHashMap.

O segundo tipo são contêineres simultâneos implementados pela tecnologia copy-on-write, cujos nomes de classe começam com CopyOnWrite, como CopyOnWriteArrayList.

A terceira categoria é a fila de bloqueio implementada por Lock . Duas condições são criadas internamente para a espera de produtores e consumidores respectivamente. Todas essas classes implementam a interface BlockingQueue, como ArrayBlockingQueue.

4.2. Diga-me a diferença entre Lista e Conjunto

marcando pontos

Interface de coleta, ordem e repetibilidade, ordem TreeSet

resposta padrão 

Tanto List quanto Set são subinterfaces da interface Collection, e sua principal diferença está na ordem e repetição dos elementos :

Lista representa uma coleção ordenada e repetível . Cada elemento da coleção possui um índice sequencial correspondente . Por padrão, ela define o índice do elemento na ordem em que o elemento é adicionado e pode acessar o elemento da coleção na posição especificada por meio de o índice. Além disso, List permite elementos repetidos.

Conjunto representa uma coleção não ordenada e não repetível . Não ordenado significa que a ordem de acesso não está na ordem de adição. A coleção Set não pode conter os mesmos elementos. Se você tentar adicionar dois elementos idênticos ao mesmo Set, ele falhará e o método de adição retornará falso. Elementos duplicados são avaliados pelo valor hashCode.

Resposta bônus – TreeSet suporta ordenação natural e classificação personalizada

Embora Set represente uma coleção não ordenada, ele possui uma classe de implementação que suporta classificação, chamada TreeSet. TreeSet pode garantir que os elementos da coleção estejam em um estado classificado e oferece suporte a dois métodos de classificação: classificação natural e classificação personalizada. Sua camada inferior é implementada por TreeMap . TreeSet também não é thread-safe, mas o valor de seus elementos internos não pode ser nulo.

4.3. Fale sobre sua compreensão de ArrayList

marcando pontos

Implementação de matriz, capacidade padrão 10, cada expansão 1,5 vezes, redução, iterador ListIterator

resposta padrão

Implementação de matriz:

ArrayList é implementado com base em um array e encapsula um array Object[] dentro de . Ao criar um contêiner por meio do construtor padrão , o array é inicializado primeiro como um array vazio e depois inicializado como um array de comprimento 10 ao adicionar dados pela primeira vez . Também podemos usar um construtor parametrizado para criar um contêiner e especificar explicitamente a capacidade do array por meio de parâmetros e, em seguida, o array é inicializado para um array com a capacidade especificada.

Cada expansão é 1,5 vezes:

Se a adição de dados ao ArrayList fizer com que o limite de comprimento do array seja excedido, a expansão automática será acionada e os dados serão adicionados. A expansão é uma cópia do array , copiando os dados do array antigo para o novo array , e o comprimento do novo array é 1,5 vezes o comprimento original .

Dimensionamento manual:

ArrayList suporta redução, mas não diminuirá automaticamente . Mesmo que haja apenas uma pequena quantidade de dados restantes no ArrayList, ele não diminuirá automaticamente. Se quisermos reduzir a capacidade de ArrayList, precisamos chamar seu método trimToSize() , então o array será reduzido de acordo com o número real de elementos, e a camada inferior também será realizada criando uma nova cópia do array.

Lista de IteradoresIterador:

Set, List e Queue são todas subinterfaces de Collection e todos herdam o método iterator() da interface pai, portanto, têm a capacidade de iterar. O mapa que usa iteradores deve ser convertido primeiro em Set por meio de entrySet() e, em seguida, usar iteradores ou para travessia.

No entanto, em comparação com as outras duas interfaces, List também fornece o método listIterator() separadamente , o que aprimora a capacidade de iteração. O método iterator() retorna um iterador Iterator, o método listIterator() retorna um iterador ListIterator e ListIterator é uma subinterface de Iterator . Com base no Iterator, ListIterator adiciona suporte para listIterator.previous() percorrer para frente e adiciona suporte para listIterator.set() para modificar dados durante a iteração .

Método de classificação:

  • O método sort() da classe de ferramenta Coleções: Collections.sort(list);
  • estilo de fluxo: list.stream().sort();
  • Comparador: list.sort(new Comparator<Integer>() {})
  • Classificação manuscrita: classificação por bolha, classificação por seleção, classificação por inserção, classificação binária, classificação rápida, classificação por heap.

conjunto de entrada() 

        HashMap<String,String> map=new HashMap<String,String>();
        map.put("aaa","bbb");map.put("cc","dd");map.put("e","f");
        Set<Map.Entry<String,String>> set=map.entrySet();
        for(Map.Entry<String,String> i:set){
            System.out.println(i.getKey()+i.getValue());
        }

listIterator()

        List<String> list = new ArrayList<String>();
        list.add(1);
        //迭代器
        Iterator<String> it=list.iterator();
        while(it.hasNext()) System.out.println(it.next());
        // 使用ListIterator向前遍历并修改元素
        ListIterator<Integer> listIterator = list.listIterator(list.size());
        while (listIterator.hasPrevious()) {
            int num = listIterator.previous();
            listIterator.set(num + 1);
        }

4.4. Diga-me a diferença entre ArrayList e LinkedList

marcando pontos

Estrutura de dados (matriz e lista vinculada), eficiência de adição e exclusão de acesso, complexidade de tempo, uso de memória

resposta padrão

Basta comparar diretamente a complexidade do espaço de matrizes e listas vinculadas e comparar a complexidade do tempo de inserção, exclusão e consulta.

1. A implementação de ArrayList é baseada em arrays, e a implementação de LinkedList é baseada em listas duplamente vinculadas .

2. Para acesso aleatório, ArrayList é melhor que LinkedList. ArrayList pode realizar acesso aleatório a elementos de acordo com o subscrito com complexidade de tempo O(1), enquanto cada elemento de LinkedList depende do ponteiro de endereço para se conectar com o próximo elemento. Pesquise o a complexidade de tempo de um elemento é O(N).

3. Para operações de inserção e exclusão, LinkedList é melhor que ArrayList, porque quando elementos são adicionados ao LinkedList em qualquer posição, não há necessidade de recalcular o tamanho ou atualizar o índice como ArrayList. A complexidade de tempo de ArrayList é O(n) ao inserir e excluir a primeira parte da lista, e a complexidade de tempo de LinkedList é O(1) porque os seguintes elementos precisam ser movidos e não há necessidade de mover. Ao inserir e excluir no final da lista, a complexidade de tempo de ambos é O(1), porque LinkedList é uma lista duplamente vinculada com um ponteiro para o nó final. 

4. LinkedList ocupa mais memória que ArrayList , pois além de armazenar dados, os nós do LinkedList também armazenam duas referências, uma apontando para o elemento anterior e outra apontando para o próximo elemento. 

Complexidade de tempo ordenada naturalmente: se ambos forem ordenados naturalmente, ArrayList pode usar pesquisa binária com uma complexidade de tempo de O (logn) e LinkedList pode usar uma tabela de salto com uma complexidade de tempo de O (logn). A camada inferior do zset do Redis é usar uma lista compactada ou uma tabela de salto.

Tabela de salto: adicione índice multinível com base na lista vinculada, pesquisa de salto:

4.5. Por favor, fale sobre o princípio subjacente do HashMap

marcando pontos

Estrutura de dados subjacente, tratamento de conflitos de tabelas hash, mecanismo de expansão de capacidade, processo put(), por que expansão de capacidade exponencial de 2, problema de loop infinito

Palavras-chave: matriz inicial 16, fator de carga 0,75, expansão exponencial de 2, endereço do nó principal da lista vinculada, comprimento da lista vinculada 8, árvore vermelha e preta, hash & (2 ^ n-1)

resposta padrão 

HashMap não é thread-safe. Em um ambiente multithread, é recomendado usar a classe de ferramenta Collections e o ConcurrentHashMap do pacote JUC.

Estrutura de dados subjacente:

No JDK8, a camada inferior do HashMap é implementada por "array + lista vinculada unidirecional + árvore vermelha e preta". A matriz é usada para pesquisa de hash, a lista vinculada é usada como método de endereço de cadeia para lidar com conflitos e a árvore vermelha e preta substitui a lista vinculada por um comprimento de 8.

Mecanismo de expansão:

No HashMap, a capacidade inicial padrão do array é 16, e essa capacidade será expandida com um expoente de 2. Especificamente, o HashMap se expandirá quando os elementos da matriz atingirem uma determinada proporção. Essa proporção é chamada de fator de carga e o padrão é 0,75.

O mecanismo de expansão automática visa garantir que o HashMap não precise ocupar muita memória no início e possa garantir espaço suficiente em tempo real durante o uso. O uso de um expoente 2 para expansão é usar operações de bits para melhorar a eficiência das operações de expansão.

Cada elemento da matriz armazena o endereço do nó principal da lista vinculada, e o método de endereço de link lida com conflitos.Se o comprimento da lista vinculada atingir 8, a árvore vermelha e preta substituirá a lista vinculada.

processo put():

Durante a execução do método put(), existem basicamente quatro etapas:

  1. Calcule a posição de acesso da chave e a operação hash&(2^n-1), que na verdade é o restante do valor do hash, e a eficiência da operação de bits é maior.
  2. Julgando a matriz, se ela estiver vazia, expanda a capacidade para a capacidade inicial de 16 pela primeira vez.
  3. Determine o nó principal da posição de acesso ao array. Se o nó principal estiver vazio, crie um novo nó de lista vinculada e armazene-o no array.
  4. Julgando o nó principal da posição de acesso da matriz, se o nó principal não estiver vazio, dependendo da situação, substitua ou insira o elemento na lista vinculada (método de inserção principal JDK7, método de inserção final JDK8), vermelho -árvore preta.
  5. Depois de inserir um elemento, julgue o número de elementos e expanda a capacidade novamente com um índice de 2 se exceder o limite.

Entre eles, a terceira etapa pode ser subdividida nas seguintes três pequenas etapas:

1. Se a chave do elemento for igual à chave do nó principal, o nó principal será substituído diretamente.

2. Se o elemento for um nó de árvore, anexe o elemento à árvore.

3. Se o elemento for um nó de lista vinculada, anexe o elemento à lista vinculada. Após anexar, é necessário avaliar o comprimento da lista vinculada para decidir se deve convertê-la em uma árvore rubro-negra. Se o comprimento da lista vinculada atingir 8 e a capacidade da matriz não atingir 64, expanda a capacidade. Se o comprimento da lista vinculada atingir 8 e a capacidade do array atingir 64, ela será convertida em uma árvore rubro-negra.

A tabela hash lida com conflitos: método de endereço aberto (detecção linear, detecção secundária, método de re-hashing), método de endereço em cadeia

Por que a capacidade do HashMap 2 está elevada à enésima potência?

Os números binários de 2^n-1 e 2^(n+1)-1 são iguais, exceto pelo primeiro dígito e pelos últimos dígitos. Desta forma, os elementos adicionados podem ser distribuídos uniformemente em cada posição do HashMap, evitando colisões de hash.

Por exemplo, 15 é 1111 em binário, 31 é 11111 em binário, 63 é 111111 em binário e 127 é 1111111 em binário.

Demonstração de hash uniforme de expansão: de 2 ^ 4 a 2 ^ 5

0 e (2 ^ 4-1) = 0;0 e (2 ^ 5-1) = 0

16&(2^4-1)=0;16&(2^5-1)=16. Portanto, após a expansão, a posição de alguns valores cuja chave é 0 permanece inalterada, e alguns valores são migrados para as novas posições após a expansão.

1 e (2 ^ 4-1) = 1; 1 e (2 ^ 5-1) = 1

17&(2^4-1)=1; Portanto, após a expansão, a posição de alguns valores cuja chave é 1 permanece inalterada, e alguns valores são migrados para as novas posições após a expansão.

Problema de loop infinito durante a expansão do JDK7

Processo de expansão de thread único: No JDK7, o método de endereço de cadeia HashMap adota o método de inserção de cabeçalho ao lidar com conflitos, e o método de inserção de cabeçalho ainda é usado durante a expansão, portanto, a ordem dos nós na lista vinculada será invertida.

Se houver dois threads T1 e T2 expandindo uma lista vinculada ao mesmo tempo, ambos marcam o nó principal e o segundo nó. Neste momento, T2 é bloqueado. Depois que T1 executa a expansão, a ordem dos nós da lista vinculada é invertido. Neste momento, T2 retoma a execução e então Flipping irá gerar uma lista vinculada circular, ou seja, B.next=A;A.next=B, portanto, um loop infinito.

Método de inserção de cauda JDK8: No JDK8, HashMap adota o método de inserção de cauda.A posição dos nós da lista vinculada não será invertida durante a expansão, o que resolve o problema do loop de expansão infinito, mas o desempenho é um pouco pior, porque a lista vinculada precisa ser percorrido para encontrar a cauda. 

Problema de cobertura de dados durante a colocação do JDK8:

HashMap não é thread-safe. Se os dados inseridos por dois threads simultâneos forem iguais após o restante do hash, poderá ocorrer substituição de dados.

O thread A é bloqueado quando encontra a posição nula da lista vinculada e está pronto para inserir, e então o thread B encontra a posição nula e a insere com sucesso. Com a recuperação do thread A, por ter julgado nulo, ele sobrescreve e insere diretamente esta posição, e sobrescreve os dados inseridos pelo thread B.

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)     // 如果没有 hash 碰撞,则直接插入
            tab[i] = newNode(hash, key, value, null);
    }

Problema de autoincremento não atômico ModCount: put executará a operação modCount++ (modCount é uma variável membro do HashMap, usada para registrar o número de vezes que o HashMap foi modificado), esta operação de etapa é dividida em ler, adicionar, salvar, não um operação atômica, mas também surge um problema de segurança de thread. 

Solução segura de thread:

  • Use Hashtable (API antiga não é recomendada)
  • Use a classe de ferramenta Collections para agrupar o HashMap em um HashMap thread-safe.
    Collections.synchronizedMap(map);
  • Use o ConcurrentHashMap mais seguro (recomendado).ConcurrentHashMap bloqueia o slot (o nó principal da lista vinculada) para garantir a segurança do thread com um pequeno desempenho.
  • Depois de usar sincronizado ou Lock para bloquear o HashMap, a operação é equivalente à execução de fila multithread (é mais problemática e não recomendada).

HashMap é baseado no algoritmo hash para determinar a posição (slot) do elemento.Quando armazenamos dados na coleção, ele calculará o valor hash da chave recebida e usará o restante do valor hash para determinar a posição de a vaga. . Caso os elementos colidam, ou seja, já existam outros elementos neste slot, o HashMap organizará esses elementos através da lista encadeada (o método de endereço em cadeia trata de conflitos ). Se a colisão se intensificar ainda mais e o comprimento de uma lista vinculada atingir 8 , o HashMap criará uma árvore vermelha e preta para substituir a lista vinculada , aumentando assim a velocidade de busca dos dados neste slot.

Ao adicionar dados ao HashMap, existem três condições que irão desencadear seu comportamento de expansão:

1. Se o array estiver vazio, execute a primeira expansão.

2. Após inserir elementos na lista vinculada, se o comprimento da lista vinculada atingir 8 e o comprimento da matriz for menor que 64, expanda a capacidade.

3. Após a adição, se os elementos da matriz ultrapassarem o limite, ou seja, a proporção ultrapassar o limite (o padrão é 0,75), a capacidade será ampliada. Além disso, cada vez que a capacidade é expandida, a capacidade é duplicada, ou seja, um novo array com o dobro do tamanho é criado e, em seguida, os arrays do array antigo são migrados para o novo array. Como a capacidade do array no HashMap é 2^N, a nova capacidade pode ser calculada por operação de deslocamento, que é muito eficiente.

Resposta bônus - HashMap não é thread-safe

HashMap não é thread-safe. Em um ambiente multithread, quando vários threads acionam a alteração do HashMap ao mesmo tempo, podem ocorrer conflitos. Portanto, não é recomendado usar HashMap em um ambiente multithread. Você pode considerar usar Collections para converter HashMap em um HashMap thread-safe. A maneira mais recomendada é usar ConcurrentHashMap.

Árvore rubro-negra: uma árvore binária aproximadamente balanceada, a diferença de altura entre as subárvores esquerda e direita pode ser maior que 1, a eficiência de pesquisa é ligeiramente inferior à de uma árvore binária balanceada, mas a eficiência de adição e exclusão é maior do que isso de uma árvore binária balanceada, adequada para inserção e exclusão frequentes.

  • Os nós são pretos ou vermelhos;
  • O nó raiz é preto e o nó folha é um nó preto vazio (frequentemente omitido);
  • Qualquer nó adjacente não pode ficar vermelho ao mesmo tempo;
  • Todos os caminhos de qualquer nó para cada uma de suas folhas contêm o mesmo número de nós pretos;
  • O desempenho da consulta é estável O(logN) e a altura é de até 2log(n+1);
     

4.6. Fale sobre ConcurrentHashMap

marcando pontos

Matriz + lista vinculada + árvore vermelho-preta, granularidade de bloqueio, mecanismo de implementação, expansão dinâmica simultânea multithread, (comprimento da matriz >>> 3) / número de núcleos de CPU, CAS

resposta padrão 

ConcurrentHashMap é uma classe do pacote de simultaneidade JUC ( java.util.concurrent ) , que é equivalente a um HashMap thread-safe. 

Matriz + lista vinculada + árvore vermelha e preta: a estrutura de dados subjacente do ConcurrentHashMap é a mesma do HashMap, que também usa "array + lista vinculada + árvore vermelha e preta"

Granularidade de bloqueio:

JDK1.7 adota bloqueio de segmento, a granularidade do bloqueio é segmentada, JDK1.8 adota sincronizado + CAS e a granularidade do bloqueio é slot (nó principal)

O método de bloqueio de slots (nós principais) durante a inserção e expansão reduz a granularidade do bloqueio e atinge a segurança do thread com menor custo de desempenho.

Mecanismo de Implementação:

  1. Ao inicializar a matriz, inicializar o nó principal e expandir e mover dados simultaneamente, ConcurrentHashMap não bloqueia, mas executa a substituição atômica na forma de CAS.
  2. Ao inserir dados, se os dados forem o nó principal da lista vinculada, eles serão CASados ​​diretamente e, se não forem o nó principal, um bloqueio sincronizado será adicionado ao nó principal.
  3. No processo de expansão, a operação de busca ainda é suportada.

O processo de inserção de elementos:
1. Ao colocar, primeiro encontre a lista vinculada correspondente por meio de hash e verifique se é o primeiro Nó. Em caso afirmativo, insira-o diretamente usando o princípio CAS sem bloqueio.

2. Se não for o primeiro nó da lista vinculada, bloqueie diretamente com o primeiro nó da lista vinculada. O bloqueio adicionado aqui é sincronizado 

Expansão simultânea multithread:

Quando um thread está inserindo dados, se descobrir que o array está se expandindo, ele participará imediatamente da operação de expansão e será inserido após concluir a expansão.

O número de migrações por thread é: (comprimento do array >>> 3) / número de núcleos de CPU.

CAS:

Realize constantemente comparação atômica e troca de variáveis ​​(compare o valor na memória com o valor esperado, troque se forem iguais e tente novamente se não forem iguais, o que significa que foram alterados por outros threads).

CAS é um bloqueio otimista e acredita com otimismo que a simultaneidade não é alta, portanto, deixe o thread continuar tentando atualizar novamente.

Vantagens: Não são utilizadas travas, portanto o desempenho é alto. CAS realiza sincronização de variáveis ​​em multithreading por meio de operação atômica de variáveis ​​sem usar bloqueios.

Desvantagens: Novas tentativas constantes em alta simultaneidade resultam em sobrecarga excessiva da CPU.

Problema ABA: O próprio mecanismo CAS não consegue detectar se o valor da variável foi modificado e se preocupa apenas com o valor atual e o valor esperado.

Por exemplo, o thread A é bloqueado logo após a leitura do valor da variável na memória. Durante o período de bloqueio, outros threads atualizam a variável e depois retomam. Achei que fosse) e trocaram.

Resolva o problema ABA: o CAS sozinho não pode resolver o problema ABA, e a referência atômica com número de versão pode ser usada para resolvê-lo. JDK5 introduz a classe AtomicStampedReference para resolver o problema ABA. Possui métodos para obter a referência do objeto atual, obter o número da versão atual, cas, etc.

Carimbado é traduzido como afixado com selos e carimbado com selo.

A lógica da estrutura de dados subjacente pode referir-se à implementação do HashMap, abaixo focarei em seu mecanismo de implementação thread-safe.

1. Ao inicializar o array ou nó principal, ConcurrentHashMap não bloqueia, mas executa a substituição atômica no modo CAS (comparar e trocar) (operação atômica, API de operação atômica baseada na classe Unsafe).

CAS, comparar e trocar, visa alcançar a sincronização variável entre vários threads sem usar bloqueios e garantir a atomicidade das operações variáveis. Um mecanismo para solucionar a perda de desempenho causada pelo uso de bloqueios no caso de paralelismo multithread. CAS é um bloqueio otimista, é otimista que a simultaneidade no programa não seja tão séria, então deixe o thread continuar tentando novamente a atualização.

Uma operação CAS consiste em três operandos – um local de memória (V), um valor antigo esperado (A) e um novo valor (B). Se o valor de um local de memória corresponder ao valor original esperado, o processador atualizará automaticamente o valor do local com o novo valor. Caso contrário, o processador não faz nada. Em ambos os casos, retorna o valor naquele local antes da instrução CAS. CAS efetivamente diz "Acho que o local V deve conter o valor A; se contiver, coloque B neste local; caso contrário, não altere esse local, apenas me diga qual é esse local agora.

2. Ao inserir dados, o processamento do bloqueio será executado, mas o bloqueio não é o array inteiro, mas o nó principal no slot. Portanto, a granularidade do bloqueio no ConcurrentHashMap é o slot, não o array inteiro, e o desempenho simultâneo é muito bom.

3. O bloqueio será executado durante a expansão e o nó principal ainda estará bloqueado. Além disso, ele oferece suporte a vários threads para expandir o array ao mesmo tempo para melhorar a simultaneidade. Cada thread precisa usar a operação CAS para capturar a tarefa primeiro e competir pelo direito de transferência de dados de um slot contínuo. Depois de capturar a tarefa, o thread bloqueará o nó principal no slot e, em seguida, migrará os dados da lista vinculada ou árvore para o novo array.

4. Não há bloqueio ao pesquisar dados , então o desempenho é muito bom. Além disso, no processo de expansão, a operação de busca ainda pode ser suportada. Se um slot não tiver sido migrado, os dados poderão ser encontrados diretamente no array antigo. Se um slot foi migrado, mas toda a expansão não terminou, o thread de expansão criará um nó de encaminhamento e o armazenará no array antigo, e então o thread de pesquisa encontrará os dados de destino do novo array de acordo com o prompt de o nó de encaminhamento.

Resposta bônus - expansão simultânea multithread ConcurrentHashMap

A dificuldade de implementar segurança de thread no ConcurrentHashMap reside na expansão simultânea de vários threads, ou seja, quando um thread está inserindo dados, se descobrir que o array está se expandindo, ele participará imediatamente da operação de expansão e, em seguida, inserirá os dados na nova matriz após a conclusão da expansão.

Durante a expansão da capacidade, vários threads compartilham tarefas de migração de dados , e o número de migrações pelas quais cada thread é responsável é: (comprimento da matriz >>> 3)/número de núcleos de CPU . Em outras palavras, as tarefas de migração atribuídas aos threads consideram integralmente as capacidades de processamento do hardware . Vários threads compartilham uniformemente parte do trabalho de migração de slot de acordo com a capacidade de processamento do hardware. Além disso, se o número calculado de migrações for inferior a 16, ele será forçado a ser alterado para 16. Isso ocorre porque, considerando a atual velocidade operacional da CPU principal no campo do servidor, poucas tarefas são processadas a cada vez, o que também é um desperdício de poder de computação da CPU.

4.7. Diga-me a diferença entre HashMap e Hashtable

marcando pontos

Segurança de thread, valor nulo, antigo esquema imaturo de sincronização de API JDK1, ConcurrentHashMap

resposta padrão

Tanto HashMap quanto Hashtable são implementações típicas de Map. A diferença entre eles é se são thread-safe e se podem armazenar valores nulos.

 1. Hashtable garante segurança de thread ao implementar a interface Map , enquanto HashMap não é thread-safe. Portanto, o desempenho do Hashtable não é tão bom quanto o do HashMap, pois sacrifica parte do desempenho para garantir a segurança do thread .

 2. Hashtable não permite armazenar nulo, não importa se nulo for usado como chave ou valor, uma exceção será lançada. E o HashMap pode armazenar nulo, seja nulo usado como chave ou valor, tudo é possível.

Resposta bônus - Hashtable é uma API antiga, ConcurrentHashMap é recomendado

Embora Hashtable seja thread-safe, ainda não é recomendado usar Hashtable em um ambiente multithread. Por ser uma API antiga que surgiu desde o Java 1.0, seu esquema de sincronização é imaturo e seu desempenho não é bom. Se você deseja usar o HashMap em um ambiente multithread, é recomendado usar o ConcurrentHashMap. Ele não apenas garante a segurança do thread, mas também melhora o desempenho do acesso simultâneo, reduzindo a granularidade dos bloqueios.

5. Genéricos 

5.1. Fale sobre genéricos e apagamento de genéricos

marcando pontos

Conceitos genéricos, escopo, apagamento genérico, benefícios, upcasting

resposta padrão

Genéricos: a parametrização de tipos específicos é um paradigma de programação que fornece um mecanismo de detecção de segurança de tipo em tempo de compilação.

Usando genéricos, podemos passar tipos de dados como parâmetros para classes, interfaces ou métodos e realizar verificação de tipo em tempo de compilação para evitar erros de conversão de tipo em tempo de execução.

O escopo dos genéricos: interface genérica, classe genérica (especifique o tipo específico ao criar um objeto), método genérico.

Método de implementação: tome a classe genérica como exemplo. Precisamos apenas usar colchetes angulares <> para agrupar um símbolo ou vários símbolos após o nome da classe, para que o símbolo possa ser usado em vez do tipo específico na classe. Ao usar uma classe genérica, qual tipo é realmente passado pelo chamador, o símbolo genérico será apagado em tempo de compilação e substituído pelo tipo real. O símbolo genérico pode ser qualquer símbolo, mas concordamos em usar símbolos como T, E, K, V, etc.

Apagamento genérico: os genéricos do Java são pseudogenéricos, porque durante a compilação do Java, todos os tipos genéricos serão apagados e convertidos em tipos comuns. 

O principal objetivo do apagamento genérico é ser compatível com versões inferiores, porque os genéricos Java são um recurso introduzido após o JDK 1.5. Para garantir que o código antigo possa ser executado normalmente, o compilador Java usa o apagamento genérico para ser compatível com o código anterior.

Comparado com a classe Object: os genéricos apagam e substituem o tipo real em tempo de compilação, mas usar a classe Object é complicado e requer conversões frequentes. Por exemplo, na coleção List, se você declarar diretamente o armazenamento da classe Object, não há problema em salvá-la e você pode transformá-la diretamente por meio do mecanismo polimórfico, mas é problemático quando você a busca. Você deve forçar a classe Object seja um objeto como String, e então você pode acessar o membro do objeto; e você não sabe se o elemento real é do tipo String ou Integer ou outros tipos, e você tem que julgar o tipo através de i instanceof String, o que é ainda mais problemático.

Benefícios genéricos:

  1. A segurança do tipo pode ser verificada em tempo de compilação.
  2. Todas as conversões são automáticas e implícitas, o que aumenta a reutilização do código.

Upcasting: Uma classe ou interface genérica pode ser atualizada para uma classe pai, mas um símbolo genérico não pode ser atualizado. Upcasting genérico refere-se ao processo de conversão de um objeto genérico em seu tipo de superclasse ou tipo de interface. Na verdade, esse processo apaga o parâmetro de tipo do objeto genérico e o reatribui à sua classe pai ou tipo de interface.

  • ArrayList<T> pode ser upcast para List<T>
  • Os genéricos ArrayList<Integer> não podem ser convertidos para cima em ArrayList<Number>. Porque ArrayList<Number> recebe ArrayList<float>, mas ArrayList<Integer> não pode receber ArrayList<Float> e não pode ser convertido de volta 

Além da transformação ascendente, há também transformação descendente. 

Classe genérica:

public class Generic<T> {
    private T t;
 
    public T getT() {
        return t;
    }
 
    public void setT(T t) {
        this.t = t;
    }
}

Interface genérica:

public interface Generic<T> {...}

Método genérico:

    public <T>void show(T t){
        System.out.println(t);
    }

Verificações de segurança em tempo de compilação:

Java introduziu genéricos na versão 1.5. Antes dos genéricos, toda vez que um objeto era lido de uma coleção, era necessário realizar uma conversão de tipo . O resultado disso era que se alguém inserisse acidentalmente um objeto do tipo errado, ocorreria um erro durante o fase de processamento de transformação em tempo de execução.

Depois de criar os genéricos , podemos informar ao compilador quais tipos de objetos são aceitos na coleção. O compilador converterá automaticamente suas inserções e informará em tempo de compilação se você inseriu um objeto do tipo errado . Isso torna o programa mais seguro e claro

Resposta bônus – Transformação ascendente

O `ArrayList<t>` na biblioteca padrão Java implementa a interface `List<t>`, que pode ser upcast para `List<t>`:
 

public class ArrayList<t> implements List<t> { ... }

List<string> list = new ArrayList<string>();

Ou seja, o tipo `ArrayList<t>` pode ser upcast para `List<t>` .

Perceber:

Não é possível fazer upcast de `ArrayList<Integer>` para `ArrayList<Number>` ou `List<Number>`. Por que é isso?

Supondo que `ArrayList<Integer>` possa ser upcast para `ArrayList<Number>`, observe o código:

// 创建ArrayList<integer>类型:
        ArrayList<Integer> integerList = new ArrayList<>(); // 添加一个Integer:
        integerList.add(new Integer(123)); // “向上转型”为ArrayList<number>:
        ArrayList<Number> numberList = integerList; // 添加一个Float,因为Float也是Number:
        numberList.add(new Float(12.34)); // 从ArrayList<integer>获取索引为1的元素(即添加的Float):
        Integer n = integerList.get(1); // ClassCastException!

Depois de convertermos um tipo `ArrayList<integer>` para `ArrayList<number>`, este `ArrayList<number>` pode aceitar o tipo `Float`, porque `Float` é uma subclasse de `Number`. Porém, `ArrayList<número>` é na verdade o mesmo objeto que `ArrayList<inteiro>`, ou seja, do tipo `ArrayList<inteiro>`, é impossível aceitar o tipo `Float`, então ao obter `Integer`, defina Uma ` ClassCastException` é gerada .

Na verdade, para evitar esse tipo de erro, o compilador não permite a conversão de `ArrayList<integer>` em `ArrayList<number>`.

IO 

6.1. Por favor, fale sobre multiplexação IO

marcando pontos

Recursos (um único thread pode lidar com várias solicitações de clientes), vantagens

resposta padrão

Multiplexação de IO: um thread lida com múltiplas operações de IO.

Um thread monitora vários identificadores de arquivo (descritores de arquivo, números inteiros positivos que marcam arquivos abertos) ao mesmo tempo. Assim que um arquivo estiver pronto (legível ou gravável), o aplicativo será notificado para realizar operações de leitura e gravação. O aplicativo é bloqueado quando nenhum identificador de arquivo está pronto, liberando recursos da CPU.

  • lO: No sistema operacional, os dados são lidos e gravados entre o estado do kernel e o estado do usuário. Na maioria dos casos, refere-se à rede IO.
  • Multidirecional: múltiplas operações de IO. Na maioria dos casos, refere-se a múltiplas conexões TCP (múltiplos soquetes ou múltiplos canais).
  • Multiplexação: Multiplexação do mesmo thread.
  • Multiplexação de IO: um thread lida com múltiplas operações de IO sem criar e manter muitos processos/threads.
  • Descritores de arquivo: descritores de gravação, descritores de leitura, descritores de exceção

Vantagens: A sobrecarga do sistema é pequena, não há necessidade de criar e manter processos ou threads adicionais e a sobrecarga causada pela troca de threads é evitada.

Três modelos para implementar multiplexação de E/S: select, poll, epoll. 

chamada de seleção: a camada inferior é uma matriz, que escuta os arquivos pesquisando e percorrendo a coleção de descritores de arquivos, verifica os descritores de arquivos prontos e notifica o aplicativo para ler e gravar. Boa plataforma cruzada (suportada por quase todas as plataformas), altos tempos de pesquisa, baixa eficiência (O(n)), alta sobrecarga de memória e número limitado de descritores de arquivos suportados.

chamada de votação: a camada inferior é uma lista vinculada unilateral. O número de tempos de pesquisa é grande, a sobrecarga do sistema é grande e o número de descritores de arquivo não é limitado (o número máximo de identificadores de arquivo é o número máximo de sistemas operacionais e a memória 1G suporta 100.000 identificadores).

chamada epoll: A estrutura de dados subjacente é uma árvore vermelho-preta mais uma lista vinculada. A árvore vermelho-preta armazena todos os eventos e a lista vinculada armazena eventos prontos. O kernel verifica a árvore vermelho-preta e insere eventos prontos no vinculado list.A chamada epoll retorna a lista de eventos prontos do kernel através da função epoll_wait e notifica as operações de leitura e gravação do aplicativo. A eficiência do retorno de chamada é alta (O(1)) e o desempenho não diminuirá à medida que o número de descritores de arquivo aumentar, e o número de descritores de arquivo não será limitado. A desvantagem é que só funciona no Linux.

A multiplexação IO é na verdade baseada na adição de um mecanismo de evento baseado em NIO. O programa registrará um conjunto de descritores de arquivo de soquete no sistema operacional e, em seguida, monitorará se há eventos IO nesses fds. Nesse caso, o programa será notificado. Os métodos de multiplexação incluem principalmente select, poll e epoll. Essas três funções serão bloqueadas, para que possam ser usadas em um loop while (true) sem deixar a CPU ociosa.

No processo de programação de E/S, quando várias solicitações de acesso do cliente precisam ser processadas ao mesmo tempo , a tecnologia multithreading ou multiplexação de E/S pode ser usada para processamento. A tecnologia de multiplexação de E/S multiplexa vários blocos de E/S no mesmo bloco de seleção, para que o sistema possa processar várias solicitações de clientes ao mesmo tempo em um caso de thread único.

Comparado com o modelo tradicional multithread/multiprocesso, a maior vantagem da multiplexação de E/S é que a sobrecarga do sistema é pequena , o sistema não precisa criar novos processos ou threads adicionais e não precisa manter a operação desses processos e threads, reduzindo a carga de trabalho de manutenção do sistema, economizando recursos do sistema.

Atualmente, as chamadas de sistema que suportam multiplexação de E/S incluem select, pselect, poll e epoll. No processo de programação de rede Linux, select tem sido usado há muito tempo para polling e notificação de eventos de rede. No entanto, alguns defeitos inerentes de select lead to Sua aplicação foi bastante restrita e, finalmente, o Linux teve que encontrar uma alternativa para selecionar na nova versão do kernel e, finalmente, escolheu o epoll.

chamada epoll

Ao chamar com epoll, não há necessidade de pesquisar o conjunto de descritores de arquivo. Em comparação com as chamadas select e poll que precisam pesquisar todo o conjunto de descritores de arquivo para aguardar a ocorrência de eventos de E/S, as chamadas epoll usam um conjunto de chamadas de sistema especializadas para gerenciar e aguardar eventos de E/S , evitando assim a necessidade de arquivo descritores A pesquisa de coleção melhora a eficiência do processamento.

Especificamente, a chamada epoll usa um conjunto de chamadas de sistema (epoll_create, epoll_ctl e epoll_wait) para implementar o mecanismo de multiplexação de E/S. Quando um aplicativo chama epoll_wait, o kernel aguardará eventos em qualquer descritor de arquivo monitorado (como eventos de soquete, eventos de pipe, etc.). Neste momento , o kernel registrará os eventos prontos em uma lista de eventos na memória e o programa aplicativo retornará a lista de eventos prontos através da função epoll_wait . Durante todo o processo de espera, o programa aplicativo não precisa pesquisar sozinho a coleção de descritores de arquivos, o que pode reduzir bastante as chamadas do sistema e o uso de recursos da CPU.

Resumindo, a chamada epoll não precisa pesquisar todo o conjunto de descritores de arquivo, o que é uma vantagem importante sobre chamadas como select e poll. Portanto, em aplicações de rede de alta simultaneidade, as chamadas epoll são mais aplicáveis ​​e eficientes.

Resposta bônus - epoll call vs.

Os princípios do epoll e do select são semelhantes.Para superar as deficiências do select, o epoll fez muitas melhorias importantes:

1. Suporta descritores de soquete ilimitados (FD) abertos por um processo O maior defeito do select é que o FD aberto por um único processo tem um certo limite , que é definido por FD_SETSIZE, e o valor padrão é 1024. Obviamente, poucos para os grandes servidores que precisam suportar dezenas de milhares de conexões TCP. Você pode optar por modificar esta macro e então recompilar o kernel, mas isso provocará uma diminuição na eficiência da rede. Também podemos resolver este problema escolhendo uma solução multiprocessos (solução Apache tradicional), mas embora o custo de criação de um processo no Linux seja relativamente pequeno, ainda não pode ser ignorado.Além disso, a troca de dados entre processos é muito problemática. Para Java Como não há memória compartilhada, a sincronização de dados precisa ser realizada por meio de comunicação Socket ou outros métodos, o que traz perda adicional de desempenho e aumenta a complexidade do programa, portanto não é uma solução perfeita. Felizmente, o epoll não tem essa limitação. O limite superior do FD que ele suporta é o número máximo de identificadores de arquivo do sistema operacional, que é muito maior que 1.024. Por exemplo, existem cerca de 100.000 identificadores em uma máquina com 1 GB de memória. O valor específico pode ser visualizado através de cat /proc/sys/fs/file-max. Geralmente, esse valor tem uma relação maior com a memória do sistema.

2. A eficiência de E/S não diminuirá linearmente com o aumento do número de FDs. Outra fraqueza fatal da seleção/poll tradicional é que quando você tem uma grande coleção de soquetes, devido ao atraso da rede ou link inativo, apenas um pequeno número de soquetes estão "ativos", mas cada chamada para seleção/pesquisa verifica linearmente todas as coleções, resultando em um declínio linear na eficiência. epoll não tem esse problema, ele opera apenas em soquetes "ativos" - isso ocorre porque o epoll é implementado de acordo com a função de retorno de chamada em cada fd na implementação do kernel, então apenas soquetes "ativos" estarão ativos Para chamar a função de retorno de chamada, outros soquetes de estado ocioso não. Neste ponto, epoll implementa um pseudo-O. O teste de benchmark para a comparação de desempenho entre epoll e select mostra que: se todos os soquetes estiverem ativos - como um ambiente LAN de alta velocidade, epoll não é muito mais eficiente que select/poll; pelo contrário, se epoll_ctl for usado demais , a eficiência ainda está lá. Ligeira queda. Mas uma vez que conexões ociosas são usadas para simular o ambiente WAN, a eficiência do epoll é muito maior do que a do select/poll.

3. Use mmap para acelerar a passagem de mensagens entre o kernel e o espaço do usuário. Seja select, poll ou epoll, o kernel precisa notificar a mensagem FD para o espaço do usuário. Como evitar cópias desnecessárias da memória é muito importante. Epoll usa o kernel e o espaço do usuário mmap implementados no mesmo bloco de memória.

4. A API do epoll é mais simples, incluindo a criação de um descritor epoll, adição de eventos de monitoramento, bloqueio e espera pela ocorrência do evento monitorado, fechamento do descritor epoll, etc.

6.2. Por favor, fale sobre BIO, NIO, O

marcando pontos

Modelo IO com bloqueio, modelo IO sem bloqueio, modelo IO assíncrono

resposta padrão

BIO: Modelo de I/O bloqueador, síncrono e bloqueador . Um thread só pode lidar com uma conexão. Quando o cliente recebe uma solicitação de conexão, o servidor precisa iniciar um thread para processamento.

Bloqueio: Se a conexão não fizer nada, o thread correspondente será sempre bloqueado, resultando em sobrecarga desnecessária do thread. 

NIO: modelo de E/S sem bloqueio, síncrono sem bloqueio , um thread pode lidar com múltiplas conexões (ou seja, solicitações) por meio de um multiplexador. A conexão enviada pelo cliente será registrada no multiplexador através do canal, e o multiplexador pesquisa todos os canais prontos para processar IO. O acesso aos dados é feito através de operações de buffer. Multiplexação IO = mecanismo de eventos NIO +.

Três componentes principais: Buffer (buffer), Canal (canal), Seletor (multiplexador).

Sem bloqueio: uma conexão não faz nada, o thread não será bloqueado e outras conexões podem ser pesquisadas.

AIO: Modelo de E/S assíncrona, assíncrona e sem bloqueio , uma solicitação válida corresponde a um thread. O programa aplicativo passa a solicitação de E/S para o sistema operacional (sistema operacional) para processamento de multiplexação e pode executar outras tarefas por si só. Após o processamento do sistema operacional, o programa aplicativo é notificado para processamento subsequente.

Assíncrono: depois que o aplicativo entrega a solicitação de IO ao sistema operacional, ele pode fazer outras coisas sozinho.

Sem bloqueio: multiplexes de sistema operacional processando solicitações de IO.

De acordo com a classificação dos modelos de E/S pela programação de rede UNIX, o UNIX fornece cinco modelos de E/S, nomeadamente modelo de E/S de bloqueio, modelo de E/S sem bloqueio, modelo de multiplexação de E/S e E/S orientada por sinal. modelo, modelo de E/S assíncrona.

Três dos cinco modelos de BIO, NIO e O, que são as abreviaturas do modelo de E/S de bloqueio, do modelo de E/S sem bloqueio e do modelo de E/S assíncrona.

BIO, modelo de bloqueio de E/S (bloqueio de E/S): É o modelo de E/S mais comumente usado . Por padrão, todas as operações de arquivo são bloqueadas.

Tomamos a interface do soquete como exemplo para entender esse modelo, ou seja, chamamos recvfrom no espaço do processo, e sua chamada de sistema não retorna até que o pacote de dados chegue e seja copiado para o buffer do processo da aplicação ou ocorra um erro, durante o qual estará sempre Esperando, o processo fica bloqueado durante todo o período desde a chamada de recvfrom até seu retorno, por isso é chamado de modelo de E/S de bloqueio.

NIO, modelo de E/S sem bloqueio (E/S sem bloqueio): Quando recvfrom vai da camada de aplicativo para o kernel, se não houver dados no buffer, ele retornará diretamente um erro EWOULDBLOCK. Geralmente, o modelo sem bloqueio O modelo de E/S é girado. Consulte para verificar esse status para ver se o kernel tem dados chegando.

AIO, modelo de E/S assíncrona (E/S assíncrona): diga ao kernel para iniciar uma operação e deixe o kernel nos notificar após a conclusão de toda a operação (incluindo a cópia de dados do kernel para o buffer do próprio usuário).

A principal diferença entre este modelo e o modelo orientado por sinal é: a E/S orientada por sinal é notificada pelo kernel quando podemos iniciar uma operação de E/S, e o modelo de E/S assíncrona é notificado pelo kernel quando o A operação de E/S foi concluída.

Resposta bônus

Modelo de multiplexação de E/S (multiplexação de E/S): o Linux fornece seleção/poll, e o processo passa um ou mais fds para a chamada do sistema select ou poll para bloquear a operação de seleção, para que select/poll possa nos ajudar a detectar se múltiplos fds estão no estado pronto. select/poll verifica se o fd está pronto sequencialmente e o número de fd suportados é limitado, portanto seu uso está sujeito a algumas restrições. O Linux também fornece uma chamada de sistema epoll, epoll usa uma abordagem orientada a eventos em vez de varredura sequencial, portanto o desempenho é maior. Quando um fd estiver pronto, chame imediatamente a função rollback.

Modelo de E/S acionado por sinal (E/S acionado por sinal): primeiro habilite a função de E/S acionada por sinal do soquete e execute uma função de processamento de sinal por meio da chamada do sistema sigaction (esta chamada do sistema retorna imediatamente, o processo continua a trabalho, é sem bloqueio). Quando os dados estão prontos, um sinal SIGIO é gerado para o processo, e a aplicação é notificada para chamar recvfrom para ler os dados através do retorno de chamada do sinal, e a função do loop principal é notificada para processar os dados.

6.3. Fale-me sobre Java NIO

marcando pontos

Sem bloqueio síncrono, Buffer, Canal, Seletor

resposta padrão

NIO: modelo de E/S sem bloqueio, síncrono sem bloqueio , um thread pode lidar com múltiplas conexões (ou seja, solicitações) por meio de um multiplexador. A conexão enviada pelo cliente será registrada no multiplexador através do canal, e o multiplexador pesquisa todos os canais prontos para processar IO. O acesso aos dados é feito através de operações de buffer. Com o surgimento do JDK1.4, a camada inferior é a chamada epoll para multiplexação IO. Multiplexação IO = mecanismo de eventos NIO +.

Três componentes principais: Buffer (buffer), Canal (canal), Seletor (multiplexador).

Sem bloqueio: uma conexão não faz nada, o thread não será bloqueado e outras conexões podem ser pesquisadas.

A biblioteca New Input/Output (NIO) foi introduzida no JDK 1.4 . O NIO compensa a deficiência da E/S de bloqueio síncrona original e fornece E/S orientada a blocos de alta velocidade em código Java padrão. Definindo classes que contêm dados e processando esses dados em partes.

NIO consiste em três componentes principais: Buffer (buffer), Canal (canal), Seletor (multiplexador).

Buffer é um objeto que contém alguns dados para serem gravados ou lidos . Adicionar o objeto Buffer na biblioteca de classes NIO reflete uma diferença importante entre a nova biblioteca e a E/S original. Na E/S orientada a fluxo, os dados podem ser gravados diretamente ou lidos diretamente no objeto Stream. Na biblioteca NIO, todos os dados são tratados com buffers . Ao ler dados, eles são lidos diretamente no buffer. Ao gravar dados, grave no buffer. Sempre que os dados no NIO são acessados, eles são operados por meio do buffer.

Canal é um canal através do qual os dados podem ser lidos e gravados. É como um cano de água, e os dados da rede são lidos e gravados através do Canal. Os canais diferem dos fluxos porque os canais são bidirecionais , os fluxos se movem apenas em uma direção e os canais podem ser usados ​​para leitura, gravação ou ambos . Como o Channel é full-duplex, ele pode mapear melhor a API do sistema operacional subjacente do que o Stream. Especialmente no modelo de programação de rede UNIX, os canais do sistema operacional subjacente são todos full-duplex e suportam operações de leitura e gravação ao mesmo tempo.

O Seletor irá sondar continuamente o Canal nele cadastrado. Caso haja novos acessos à conexão TCP, eventos de leitura e escrita em um Canal, o Canal estará no estado pronto e será sondado pelo Seletor , sendo então obtido através do SelectionKey A coleção de canais prontos para operações de E/S subsequentes. Um seletor multiplexador pode pesquisar vários canais ao mesmo tempo. Como o JDK usa epoll() em vez da implementação de seleção tradicional, ele não tem um limite máximo de manipulação de conexão de 1024/2048. Isso também significa que apenas um thread é necessário para ser responsável pela pesquisa do Seletor, e milhares de clientes podem ser acessados, o que é de fato uma grande melhoria.

Resposta bônus

NIO2 do Java 7 fornece suporte a canal assíncrono, que pode fornecer IO mais eficiente. Esse mecanismo de IO baseado em canal assíncrono também é chamado de IO assíncrono (AsynchronousIO). NIO2 fornece duas interfaces e três classes de implementação para O, entre as quais AsynchronousSocketChannel e AsynchronousServerSocketChannel são canais assíncronos que suportam comunicação TCP.

7. Anormal  

7.0. Fale sobre o sistema de exceções

 Throwable, Error, Exception, exceção em tempo de compilação, RuntimeException, RuntimeException comum

Classe jogável

Throwable é a classe base de todas as exceções e erros (Exception e Error), traduzida como throwable.

Os dois métodos mais importantes de Throwable são:

  • getMessage(): Retorna a string de detalhes deste lançável.

  • printStackTrace(): envia as informações de rastreamento de pilha do lançável para a saída de erro padrão. Ele contém o nome da classe do lançável, a mensagem e a sequência de chamadas para cada método.

Classe de erro

A classe Error em Java representa condições de erro graves, geralmente causadas pela falha da máquina virtual ou de outras camadas subjacentes, como estouro de memória e estouro de pilha, que fará com que o aplicativo seja encerrado.

Normalmente, o programa não deve capturar Error, e OutOfMemoryError pode ser capturado em situações específicas para lidar com estouro de memória. Use o bloco try-catch-finalmente para capturar exceções e executar operações como limpeza de recursos, destruição, relatório de erros e encerramento de aplicativos no bloco final.

Erros comuns incluem:

  • OutOfMemoryError: Erro de falta de memória, geralmente causado por um aplicativo que tenta alocar mais memória do que a disponível.
  • StackOverflowError: Um erro de estouro de pilha ocorre quando o espaço de pilha necessário para uma chamada recursiva de método se esgota.
  • NoClassDefFoundError: Um erro de classe não encontrada, geralmente causado pela JVM não conseguir encontrar uma classe que o aplicativo está tentando usar.
  • UnsatisfiedLinkError: Erros de link insatisfeito, geralmente causados ​​por problemas de link ao chamar métodos nativos.

Classe de exceção 

As subclasses de Exception incluem exceções em tempo de compilação e exceções em tempo de execução:

  • Exceção em tempo de compilação: uma exceção que pode ser detectada durante a fase de compilação. Por exemplo, FileNotFoundException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, SQLException, ParseException (exceção de análise), etc. Se o programa for lidar com essas exceções, ele deverá usar explicitamente um bloco de instrução try-catch ou usar a cláusula throws na definição do método para declarar a exceção.

  • Exceção de tempo de execução: uma exceção que ocorre apenas em tempo de execução. Por exemplo, NullPointerException, ArrayIndexOutOfBoundsException, etc. Essas exceções geralmente são causadas por erros lógicos no código do programa, que não são solicitados durante a programação, mas apenas relatados durante o tempo de execução. Portanto, ao escrever um programa, essas exceções geralmente não podem ser tratadas, mas depois que o programa é desenvolvido, é necessário algum tratamento dessas exceções para evitar que o programa trave durante a execução.

    • NullPointerException Exceção de ponteiro nulo ; causa: acesso a um objeto não inicializado ou a um objeto que não existe.
    • ClassNotFoundException A classe não pode encontrar uma exceção ; o motivo: o nome e o caminho da classe foram carregados incorretamente;
    • NumberFormatException A formatação do número é anormal ; causa: o tipo de caractere convertido em um número contém caracteres não numéricos.
    • IndexOutOfBoundsException Exceção de índice fora dos limites ; causa: acessando elementos fora dos limites da matriz
    • IllegalArgumentException Exceção de parâmetro ilegal . Causa: Um parâmetro inválido foi passado
    • MethodArgumentNotValidException O parâmetro do método é inválido . Motivo: falha na verificação JSR303
    • Exceção de conversão de classe ClassCastException . Motivo: Um erro é relatado quando o objeto é forçado a ser um objeto sem relacionamento de herança. Esta exceção é um erro relatado ao verificar o relacionamento de herança durante a fase de verificação de metadados do processo de carregamento de classe.
    • ArithmeticException Exceção aritmética . Causa: Ao dividir por 0.  

7.1. Fale sobre o mecanismo de tratamento de exceções do Java

marcando pontos

Tratamento de exceções, proibição de lançamento ou retorno finalmente, lançamento de exceções, pilha de rastreamento de exceções, tratamento unificado de exceções

resposta padrão 

O mecanismo de exceção do Java pode ser dividido em três partes: tratamento de exceções, lançamento de exceções e pilha de rastreamento de exceções.

Tratamento de exceções: a instrução para tratar exceções consiste em três partes: try, catch e finalmente. O bloco try é usado para encapsular o código de negócios, o bloco catch é usado para capturar e tratar uma exceção e o bloco finalmente é usado para reciclar recursos.

Proibir lançamento ou retorno finalmente: evita que exceções de lançamento em try-catch falhem. Se você lançar ou retornar finalmente, sairá da exceção diretamente e não poderá voltar para tentar ou capturar para executar retorno ou lançamento. Em circunstâncias normais, quando o bloco final é executado, o sistema volta para executar a instrução return ou throw no bloco try ou bloco catch novamente.

Lançamento de uma exceção: lançamentos só podem ser usados ​​na assinatura do método, e múltiplas exceções podem ser lançadas, indicando a possibilidade de uma exceção. Lançar significa lançar uma determinada instância de exceção.

Rastreamento de pilha de exceção:

Quando ocorre uma exceção no programa, as informações da pilha de rastreamento de exceção serão impressas.De acordo com as informações da pilha de rastreamento, podemos encontrar a localização da exceção e rastrear o processo de propagação da exceção para o método da camada superior.

A ordem de propagação da exceção é oposta à ordem de chamada dos métodos, começa no método onde ocorre a exceção, continua a se propagar para o método superior que a chama e, finalmente, passa para o método principal. Se ainda não for processado, a JVM encerra o programa e imprime as informações da pilha de rastreamento de exceção.

A especificação de log não recomenda o uso de e.printStackTrace(): altere para log.error("Seu programa tem uma exceção", e);
confusão de log: o log de pilha impresso por e.printStackTrace() é intercalado com o código de negócios log Juntos, geralmente não é conveniente verificar o log de exceções.
Problemas de desempenho: O método printStackTrace() irá gerar um grande número de objetos string, o que terá um certo impacto no desempenho do sistema.
 

Tratamento unificado de exceções:

  1. O módulo público cria uma classe de enumeração de códigos de erro, que geralmente têm cinco dígitos, os dois primeiros dígitos representam diferentes cenários de negócios e os últimos três dígitos representam códigos de erro;
  2. Crie uma classe de tratamento de exceções no pacote de exceções de cada módulo, anotação de classe
    @RestControllerAdvice, cada anotação de método de tratamento de exceções @ExceptionHandler, retorne a classe de resultado com código de erro após o processamento;
  3. Quando uma exceção ocorre ou é lançada durante o desenvolvimento real, ela será interceptada e processada diretamente sem try-catch.

O mecanismo de tratamento de exceções pode fazer com que o programa tenha excelente tolerância a falhas e robustez.Quando o programa é executado em uma situação inesperada, o sistema irá gerar um objeto Exception para notificar o programa, de modo a realizar a "parte de realização da função de negócios do código" e "erro Lidar com parte do código" separação, para que o programa possa obter melhor legibilidade.

O mecanismo de exceção do Java pode ser dividido em três partes : tratamento de exceções, lançamento de exceções e problemas de pilha de rastreamento de exceções .

manipulação de exceção 

A instrução para tratar exceções consiste em três partes: try, catch e finalmente. O bloco try é usado para encapsular o código de negócios, o bloco catch é usado para capturar e tratar um certo tipo de exceção e o bloco finalmente é usado para reciclar recursos. Se ocorrer uma exceção no código de negócios, o sistema criará um objeto de exceção e enviará o objeto de exceção para a JVM, e então a JVM encontrará um bloco catch que pode lidar com a exceção e entregará o objeto de exceção ao bloco catch para em processamento. Se a JVM não encontrar um bloco de código catch que possa tratar a exceção, o ambiente de tempo de execução será encerrado e o programa Java será encerrado. Se o código de negócios abrir um recurso, ele poderá ser fechado no bloco final, pois o bloco final será executado independentemente de ocorrer uma exceção (em geral).

Lançar uma exceção 

Quando ocorre um erro no programa, o sistema lançará automaticamente uma exceção. Além disso, Java também permite que programas lancem exceções ativamente . No código comercial, quando a condição para julgar um erro for verdadeira, você pode usar a palavra-chave throw para lançar uma exceção . Nesse caso, se o método atual não souber como tratar a exceção, você pode declarar uma exceção a ser lançada por meio da palavra-chave throws na assinatura do método , e a exceção será entregue à JVM para processamento .

rastreamento de pilha de exceção

Quando o programa está em execução, ocorre frequentemente uma série de chamadas de método, formando assim uma pilha de chamadas de método. O mecanismo de exceção fará com que as exceções se propaguem entre esses métodos, e a ordem de propagação das exceções é oposta à das chamadas de método . A exceção se propaga para fora do método onde ocorreu a exceção , primeiro para o chamador do método, depois para o chamador superior e assim por diante. Por fim, será passado para o método principal , se ainda não for processado, a JVM encerrará o programa e imprimirá as informações da pilha de rastreamento de exceção.

Exemplo:

public class Test {
    public static void main(String[] args) {
        fun();
    }

    private static void fun() {
        fun1();
    }

    private static void fun1() {
        System.out.println(6/0);
    }
}

Primeiro imprima o fun1() problemático, depois imprima o fun2() que o chama e, em seguida, imprima o método principal que o chama:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at package1.Test.fun1(Test.java:17)
	at package1.Test.fun(Test.java:13)
	at package1.Test.main(Test.java:9)

Resposta bônus - a diferença entre arremesso e arremesso, evite usar retorno ou arremesso no bloco final

lança:

 - só pode ser usado em assinaturas de métodos

 - Múltiplas exceções podem ser declaradas, separadas por vírgulas

 - Indica que o método atual não sabe como tratar a exceção , que é tratada pelo chamador do método (se o método mn não souber como tratar a exceção, a exceção será entregue à JVM, e o A maneira como a JVM lida com a exceção é imprimir as informações da pilha de rastreamento de exceção e encerrar o programa, e é por isso que o programa terminará automaticamente quando encontrar uma exceção)

- throws indica uma possibilidade  de exceções , e essas exceções não ocorrem necessariamente

lançar:

 - Indica que algum tipo de objeto de exceção é lançado no método e a instrução throw pode ser usada sozinha.

 - A instrução throw lança uma instância de exceção , não uma classe de exceção, e apenas uma instância de exceção pode ser lançada por vez

 - A execução do throw deve ter gerado algum tipo de exceção

Evite usar return ou lançar bloco final

Quando um programa Java executa um bloco try ou um bloco catch e encontra uma instrução return ou throw , essas duas instruções farão com que o método termine imediatamente , mas o sistema não encerrará o método executando essas duas instruções, mas procurará o tratamento de exceções Se o bloco final está incluído , se não houver bloco final, o programa executa imediatamente a instrução return ou throw e o método termina; se houver um bloco final, o sistema imediatamente começa a executar o bloco final . Somente após a execução do bloco finalmente ser concluída , o sistema voltará para executar a instrução return ou throw no bloco try e no bloco catch novamente ;

如果finally块里也使用了return或throw等语句,finally块会终止方法,系统将不会跳回去执行try块、catch块里的任何代码。这将会导致try块、catch块中的return、throw语句失效,所以,我们应该尽量避免在finally块中使用return或throw。

finally代码块不执行的几种情况:

 - 如果当一个线程在执行 try 语句块或者catch语句块时被打断interrupted或者被终止killed,与其相对应的 finally 语句块可能不会执行。

 - 如果在try块或catch块中使用 `System.exit(1);` 来退出虚拟机,则finally块将失去执行的机会。

八、反射 

8.1.请说说你对反射的了解

得分点

反射概念、通过反射机制可以实现、获取Class对象的三种方式、优缺点、应用场景

反射:在程序运行期间动态地获取类的信息并对类进行操作的机制。

通过反射机制可以实现:

  • 获取类或对象的Class对象:程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的所有方法和属性(包括私有,私有需要给该字段调用setAccessible(true)方法开启私有权限)。注意类的class对象是运行时生成的,类的class字节码文件是编译时生成的。
  • Crie uma instância: Quando o programa está em execução, você pode usar a reflexão para criar primeiro o objeto Class da classe, depois criar a instância da classe e acessar os membros da instância; Xxx.class.newInstance(); por exemplo , no código-fonte da classe contêiner Spring, a instanciação do Bean é Passar o objeto Class da classe Bean. O objeto Class da classe Bean é obtido da variável de membro de tipo do objeto BeanDefinition. O objeto BeanDefinition armazena informações de declaração, como alguns tipos de Bean, nomes e escopos.
  • Gerando uma classe ou objeto proxy dinâmico: Quando o programa está em execução, uma classe proxy dinâmica ou um objeto proxy dinâmico de uma classe pode ser gerado por meio do mecanismo de reflexão. Por exemplo, o método estático newProxyInstance da classe Proxy no JDK pode ser usado para criar um objeto proxy dinâmico baseado em interface.

Obtenha a camada inferior da JVM do objeto Class: Se a classe não tiver sido carregada, o processo de carregamento da classe será implementado primeiro através da JVM, ou seja, carregamento, vinculação (verificação, preparação, análise), inicialização e o objeto Class da classe será gerada durante a fase de carregamento.

O método de obtenção do objeto Class Class: dog.getClass();Dog.class;Class.forName("package1.Dog");

Vantagens e desvantagens da reflexão:

  • Prós: mais flexível. As classes podem ser adquiridas dinamicamente durante o tempo de execução, melhorando a flexibilidade do código.
  • Desvantagens: Baixo desempenho. O desempenho é muito pior do que o código Java direto.  

Cenário de aplicação:

  • JDBC carrega o driver de banco de dados: Ao usar JDBC, se você deseja criar uma conexão de banco de dados, primeiro você precisa carregar o driver de banco de dados por meio do mecanismo de reflexão;
  • Ciclo de vida do feijão:
    • Instancie as classes analisadas a partir de xml: a maioria das estruturas suporta anotações ou configurações XML para definir classes no aplicativo. As classes analisadas a partir de configurações xml são strings que precisam ser instanciadas usando o mecanismo de reflexão; por exemplo, Spring passa <bean id= "xx" class="nome completo da classe qualificada"> e <property name="injetar por nome" ref="id do Bean injetado"> definem o bean e, em seguida, obtêm a classe por Class.forName("xxx.Xxx") objeto de classe e, em seguida, crie uma instância.
    • Anote a classe contêiner para carregar o Bean e instanciar o Bean: No ciclo de vida do Bean, o método de construção da classe contêiner de anotação percorrerá o arquivo .class em @ComponentScan("scanning path"), através da classe loader.load("class name") O objeto de classe da classe é obtido por meio do método e armazenado no beanDefinitionMap. Em seguida, percorra o beanDefinitionMap, instancie através do objeto de classe, etc.
  • AOP cria um objeto proxy dinâmico: A implementação da programação orientada a aspectos (AOP) consiste em criar um objeto proxy do objeto alvo quando o programa está em execução, que deve ser realizado pelo mecanismo de reflexão. 

Processo de carregamento de classe: carregamento, vinculação (validação, preparação, resolução), inicialização. Este processo é feito no subsistema de carregamento de classes.

Carregando: Gere o objeto Class da classe.

  1. Obtenha o fluxo de bytes binários que define esta classe pelo seu nome totalmente qualificado
  2. Converta a estrutura de armazenamento estático representada por este fluxo de bytes na estrutura de dados de tempo de execução da área de método. Inclui a criação de um conjunto de constantes de tempo de execução e a colocação de algumas referências de símbolos do conjunto de constantes de classe no conjunto de constantes de tempo de execução.
  3. Um objeto java.lang.Class representando esta classe é gerado na memória como uma entrada de acesso para diversos dados desta classe na área de métodos. Observe que o objeto de classe de uma classe é gerado em tempo de execução e o arquivo de bytecode de classe de uma classe é gerado em tempo de compilação.

Vinculação: Mescla os dados binários da classe no JRE. O processo é dividido nas seguintes 3 etapas:

  • Verificação: certifique-se de que o código esteja em conformidade com a especificação da máquina virtual JAVA e as restrições de segurança. Incluindo verificação de formato de arquivo, verificação de metadados, verificação de bytecode, verificação de referência de símbolo.
    • Verificação de formato de arquivo: verifique se o fluxo de bytes está em conformidade com a especificação de formato de arquivo de classe. Por exemplo, se o número da versão está no intervalo de compatibilidade da JVM.
    • Verificação de metadados: Metadados são o nome completo da classe, informações do método, informações do campo, relacionamento de herança, etc. Por exemplo, verifique se o nome da classe, o nome da interface e o identificador estão em conformidade com a especificação, se todos os métodos da interface são implementados, se todos os métodos abstratos da classe abstrata são implementados, se a classe final é herdada, etc.
    • Verificação de bytecode: verifique o bytecode para garantir que a classe verificada não executará comportamentos perigosos para a JVM em tempo de execução. Por exemplo, converta à força o objeto da classe pai em um objeto de subclasse (somente quando a referência da classe pai aponta para o objeto da subclasse ele pode ser downcast).
    • Verificação de referência de símbolo: Verifique se o objeto referenciado existe, se tem direito de referência, etc.
  • Preparação: Aloque memória para variáveis ​​de classe (ou seja, variáveis ​​estáticas) e atribua valores zero.
  • Análise: Converta as referências simbólicas (nomes de classes, nomes de membros, identificadores) no pool de constantes de tempo de execução de área de método em referências diretas (endereços de memória reais, que não contêm nenhuma informação abstrata, para que possam ser usados ​​diretamente).

Inicialização: Atribua valores iniciais às variáveis ​​de classe e execute blocos de instruções estáticas.

aop:

Uma ideia de programação que aprimora o código sem alterar o código original.

  • Objeto alvo (Target): A função original remove o objeto gerado pela classe correspondente à função comum. Este tipo de objeto não pode completar diretamente o trabalho final
  • Proxy (Proxy): O objeto de destino não pode concluir o trabalho diretamente e precisa ser preenchido com funções, o que é realizado por meio do objeto proxy do objeto original

Spring AOP aprimora o design original (código) sem alterá-lo. Sua camada subjacente é implementada no modo proxy. Portanto, para aprimorar o objeto original, é necessário criar um objeto proxy para o objeto original. No proxy O método em o objeto adiciona o conteúdo da notificação [como: o método do método em MyAdvice] e o aprimoramento é realizado. Isso é o que chamamos de proxy (Proxy).

reflexão

Em um programa Java, muitos objetos terão exceções em tempo de compilação e exceções em tempo de execução. Por exemplo, no caso do polimorfismo, Car c = new Audi(); Esta linha de código irá gerar uma variável c em tempo de execução, e a variável em tempo de compilação O tipo da variável é Car, e o tipo de variável é Audi em tempo de execução; além disso, há casos mais extremos, como o programa recebe um objeto passado de fora em tempo de execução, e o tipo em tempo de compilação deste objeto é Object, mas o programa precisa chamar este método do tipo de tempo de execução Object, neste caso, existem duas soluções:

A primeira abordagem é assumir que as informações específicas do tipo são totalmente conhecidas em tempo de compilação e tempo de execução.Neste caso, o operador instanceof pode ser usado para julgar primeiro e, em seguida, o cast pode ser usado para convertê-lo em uma variável de seu tipo de tempo de execução. A segunda abordagem é que é impossível prever a quais classes o objeto e a classe podem pertencer em tempo de compilação, e o programa depende apenas de informações de tempo de execução para descobrir as informações reais do objeto e da classe, o que requer reflexão.

Especificamente, através do mecanismo de reflexão, podemos realizar as seguintes operações:

- Quando o programa está em execução, você pode obter o objeto Class de qualquer classe através da reflexão, e visualizar as informações desta classe através deste objeto;

- Quando o programa está em execução, você pode criar uma instância de qualquer classe através da reflexão e acessar os membros da instância; - Quando o programa está em execução, você pode gerar uma classe proxy dinâmica ou objeto proxy dinâmico de uma classe através da reflexão mecanismo.

Resposta bônus – cenário de aplicação de reflexão

O mecanismo de reflexão do Java é amplamente utilizado em projetos reais.Cenários de aplicação comuns incluem:

- Ao usar JDBC, se você deseja criar uma conexão com o banco de dados, primeiro você precisa carregar o driver do banco de dados através do mecanismo de reflexão;

- A maioria dos frameworks suporta configuração de anotação/XML, e a classe analisada a partir da configuração é uma string, que precisa ser instanciada usando o mecanismo de reflexão;

- A implementação da programação orientada a aspectos (AOP) consiste em criar a classe proxy do objeto alvo quando o programa está em execução, o que deve ser realizado pelo mecanismo de reflexão. 

Nove, multithreading

[Ensaio de estereótipo de entrevista Java 2023] Artigos Java Multithreading - Blog de vincewm - Blog CSDN

Acho que você gosta

Origin blog.csdn.net/qq_40991313/article/details/130506034
Recomendado
Clasificación