JavaRush /Blogue Java /Random-PT /Análise de perguntas e respostas de entrevistas para dese...

Análise de perguntas e respostas de entrevistas para desenvolvedor Java. Parte 15

Publicado no grupo Random-PT
Olá, olá! Quanto um desenvolvedor Java precisa saber? Você pode discutir muito sobre esse assunto, mas a verdade é que na entrevista você será levado ao máximo pela teoria. Mesmo naquelas áreas do conhecimento que você não terá oportunidade de utilizar em seu trabalho. Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 15 - 1Bom, se você é iniciante, seu conhecimento teórico será levado muito a sério. Como ainda não há experiência e grandes conquistas, resta verificar a solidez da base de conhecimento. Hoje continuaremos a fortalecer essa base examinando as perguntas de entrevista mais populares para desenvolvedores Java. Vamos voar!

Núcleo Java

9. Qual é a diferença entre ligação estática e dinâmica em Java?

Já respondi essa pergunta neste artigo na questão 18 sobre polimorfismo estático e dinâmico, aconselho a leitura.

10. É possível utilizar variáveis ​​privadas ou protegidas em uma interface?

Não, você não pode. Porque quando você declara uma interface, o compilador Java adiciona automaticamente as palavras-chave public e abstract antes dos métodos de interface e as palavras-chave public , static e final antes dos membros de dados. Na verdade, se você adicionar private ou protected , surgirá um conflito e o compilador reclamará do modificador de acesso com a mensagem: “Modifier '<selected modifier>' not enabled here.” Por que o compilador adiciona public , static e final variáveis ​​na interface? Vamos descobrir:
  • público - a interface permite que o cliente interaja com o objeto. Se as variáveis ​​não fossem públicas, os clientes não teriam acesso a elas.
  • static - interfaces não podem ser criadas (ou melhor, seus objetos), então a variável é estática.
  • final - como a interface é utilizada para atingir 100% de abstração, a variável tem sua forma final (e não será alterada).

11. O que é Classloader e para que é utilizado?

Classloader – ou Class Loader – fornece carregamento de classes Java. Mais precisamente, o carregamento é fornecido por seus descendentes - carregadores de classes específicos, porque O próprio ClassLoader é abstrato. Cada vez que um arquivo .class é carregado, por exemplo, após chamar um construtor ou método estático da classe correspondente, esta ação é executada por um dos descendentes da classe ClassLoader . Existem três tipos de herdeiros:
  1. Bootstrap ClassLoader é um carregador básico, implementado no nível da JVM e não possui feedback do ambiente de execução, pois faz parte do kernel da JVM e é escrito em código nativo. Este carregador serve como pai de todas as outras instâncias do ClassLoader.

    Principalmente responsável por carregar classes internas do JDK, geralmente rt.jar e outras bibliotecas centrais localizadas no diretório $JAVA_HOME/jre/lib . Diferentes plataformas podem ter diferentes implementações deste carregador de classes.

  2. Extension Classloader é um carregador de extensão, um descendente da classe do carregador base. Cuida do carregamento da extensão das classes base Java padrão. Carregado a partir do diretório de extensões JDK, normalmente $JAVA_HOME/lib/ext ou qualquer outro diretório mencionado na propriedade de sistema java.ext.dirs (esta opção pode ser usada para controlar o carregamento de extensões).

  3. System ClassLoader é um carregador de sistema implementado no nível JRE que se encarrega de carregar todas as classes de nível de aplicativo na JVM. Ele carrega arquivos encontrados na variável de ambiente de classe -classpath ou na opção de linha de comando -cp .

Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 15 - 2Os carregadores de classe fazem parte do tempo de execução Java. No momento em que a JVM solicita uma classe, o carregador de classes tenta localizar a classe e carregar a definição de classe no tempo de execução usando o nome completo da classe. O método java.lang.ClassLoader.loadClass() é responsável por carregar a definição da classe em tempo de execução. Ele tenta carregar uma classe com base em seu nome completo. Se a classe ainda não tiver sido carregada, ela delega a solicitação ao carregador de classes pai. Este processo ocorre recursivamente e se parece com isto:
  1. O System Classloader tenta encontrar a classe em seu cache.

    • 1.1. Se a classe for encontrada, o carregamento será concluído com sucesso.

    • 1.2. Se a classe não for encontrada, o carregamento será delegado ao Extension Classloader.

  2. O Extension Classloader tenta encontrar a classe em seu próprio cache.

    • 2.1. Se a classe for encontrada, ela será concluída com êxito.

    • 2.2. Se a classe não for encontrada, o carregamento é delegado ao Bootstrap Classloader.

  3. Bootstrap Classloader tenta encontrar a classe em seu próprio cache.

    • 3.1. Se a classe for encontrada, o carregamento será concluído com sucesso.

    • 3.2. Se a classe não for encontrada, o Bootstrap Classloader subjacente tentará carregá-la.

  4. Se estiver carregando:

    • 4.1. Bem-sucedido – o carregamento da classe foi concluído.

    • 4.2. Se falhar, o controle será transferido para o Extension Classloader.

  5. 5. O Extension Classloader tenta carregar a classe e, se estiver carregando:

    • 5.1. Bem-sucedido – o carregamento da classe foi concluído.

    • 5.2. Se não tiver êxito, o controle será transferido para o System Classloader.

  6. 6. O System Classloader tenta carregar a classe e, se estiver carregando:

    • 6.1. Bem-sucedido – o carregamento da classe foi concluído.

    • 6.2. Não foi aprovado com êxito - uma exceção foi gerada - ClassNotFoundException.

O tópico dos carregadores de classe é vasto e não deve ser negligenciado. Para conhecê-lo com mais detalhes, aconselho você a ler este artigo , e não vamos nos demorar e seguir em frente.

12. O que são áreas de dados em tempo de execução?

Ares de dados em tempo de execução - áreas de dados em tempo de execução JVM. A JVM define algumas áreas de dados de tempo de execução necessárias durante a execução do programa. Alguns deles são criados quando a JVM é iniciada. Outros são thread-local e são criados somente quando o thread é criado (e destruídos quando o thread é destruído). As áreas de dados de tempo de execução da JVM são assim: Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 15 - 3
  • PC Register é local para cada thread e contém o endereço da instrução JVM que o thread está executando atualmente.

  • JVM Stack é uma área de memória usada como armazenamento para variáveis ​​locais e resultados temporários. Cada thread tem sua própria pilha separada: assim que o thread termina, essa pilha também é destruída. Vale a pena notar que a vantagem da pilha sobre o heap é o desempenho, enquanto o heap certamente tem uma vantagem na escala de armazenamento.

  • Pilha de métodos nativos - Uma área de dados por thread que armazena elementos de dados, semelhantes à pilha JVM, para executar métodos nativos (não Java).

  • Heap - usado por todos os threads como um armazenamento que contém objetos, metadados de classe, arrays, etc., que são criados em tempo de execução. Esta área é criada quando a JVM é iniciada e é destruída quando ela é encerrada.

  • Área de método - Esta área de tempo de execução é comum a todos os threads e é criada quando a JVM é iniciada. Ele armazena estruturas para cada classe, como Runtime Constant Pool, código para construtores e métodos, dados de métodos, etc.

13. O que é um objeto imutável?

Nesta parte do artigo, nas questões 14 e 15, já existe uma resposta para essa pergunta, então dê uma olhada sem perder tempo.

14. O que há de especial na classe String?

Anteriormente na análise, falamos repetidamente sobre certos recursos do String (havia uma seção separada para isso). Agora vamos resumir os recursos de String :
  1. É o objeto mais popular em Java e é usado para diversos propósitos. Em termos de frequência de uso, não é inferior nem mesmo aos tipos primitivos.

  2. Um objeto desta classe pode ser criado sem usar a palavra-chave new - diretamente através de aspas String str = “string”; .

  3. String é uma classe imutável : ao criar um objeto desta classe, seus dados não podem ser alterados (quando você adiciona + “outra string” a uma determinada string, como resultado você obterá uma nova terceira string). A imutabilidade da classe String a torna segura para threads.

  4. A classe String é finalizada (possui o modificador final ), portanto não pode ser herdada.

  5. String tem seu próprio pool de strings, uma área de memória no heap que armazena em cache os valores de string que ela cria. Nesta parte da série , na questão 62, descrevi o pool de strings.

  6. Java possui análogos de String , também projetados para trabalhar com strings - StringBuilder e StringBuffer , mas com a diferença de que são mutáveis. Você pode ler mais sobre eles neste artigo .

Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 15 - 4

15. O que é covariância de tipo?

Para entender a covariância, veremos um exemplo. Digamos que temos uma classe de animais:
public class Animal {
 void voice() {
   System.out.println("*тишина*");
 }
}
E alguma classe Dog estendendo-a :
public class Dog extends Animal {

 @Override
 public void voice() {
   System.out.println("Гав, гав, гав!!!");
 }
}
Como lembramos, podemos facilmente atribuir objetos do tipo herdeiro ao tipo pai:
Animal animal = new Dog();
Isso nada mais será do que polimorfismo. Conveniente e flexível, não é? Bem, e a lista de animais? Podemos fornecer uma lista com um Animal genérico e uma lista com objetos Dog ?
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;
Neste caso, a linha de atribuição da lista de cães à lista de animais estará sublinhada em vermelho, ou seja. o compilador não passará esse código. Apesar de esta atribuição parecer bastante lógica (afinal, podemos atribuir um objeto Dog a uma variável do tipo Animal ), ela não pode ser feita. Isso ocorre porque se fosse permitido, poderíamos colocar um objeto Animal em uma lista que originalmente deveria ser um Dog , pensando que só tínhamos Dogs na lista . E então, por exemplo, usaremos o método get() para pegar um objeto daquela lista de cães , pensando que é um cachorro, e chamar nele algum método do objeto Cachorro , que Animal não possui . E como você entende, isso é impossível - ocorrerá um erro. Mas, felizmente, o compilador não deixa escapar esse erro lógico ao atribuir uma lista de descendentes a uma lista de pais (e vice-versa). Em Java, você só pode atribuir objetos de lista a variáveis ​​de lista com genéricos correspondentes. Isso é chamado de invariação. Se eles pudessem fazer isso, isso seria chamado e é chamado de covariância. Ou seja, covariância é se pudéssemos definir um objeto do tipo ArrayList<Dog> para uma variável do tipo List<Animal> . Acontece que a covariância não é suportada em Java? Não importa como seja! Mas isso é feito de uma maneira especial. Para que serve o desenho ? estende Animal . É colocado com um genérico da variável à qual queremos definir o objeto lista, com um genérico do descendente. Esta construção genérica significa que qualquer tipo que seja descendente do tipo Animal servirá (e o tipo Animal também se enquadra nesta generalização). Por sua vez, Animal pode ser não apenas uma classe, mas também uma interface (não se deixe enganar pela palavra-chave extends ). Podemos fazer nossa tarefa anterior assim: Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 15 - 5
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
Como resultado, você verá no IDE que o compilador não reclamará dessa construção. Vamos verificar a funcionalidade deste design. Digamos que temos um método que faz com que todos os animais passados ​​para ele emitam sons:
public static void animalsVoice(List<? extends Animal> animals) {
 for (Animal animal : animals) {
   animal.voice();
 }
}
Vamos dar a ele uma lista de cães:
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
animalsVoice(dogs);
No console veremos a seguinte saída:
Uau, au, au!!! Uau, au, au!!! Uau, au, au!!!
Isto significa que esta abordagem à covariância funciona com sucesso. Deixe-me observar que este genérico está incluído na lista ? estende Animal não podemos inserir novos dados de qualquer tipo: nem do tipo Dog , nem mesmo do tipo Animal :
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
animals.add(new Dog());
dogs.add(new Animal());
Na verdade, nas duas últimas linhas o compilador irá destacar a inserção de objetos em vermelho. Isso se deve ao fato de que não podemos ter cem por cento de certeza de qual lista de objetos de que tipo será atribuída à lista com dados pelo genérico <? estende Animal> . Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 15 - 6Gostaria também de falar sobre contravariância , já que normalmente esse conceito sempre anda junto com a covariância, e via de regra são questionados sobre eles em conjunto. Este conceito é um pouco oposto à covariância, uma vez que este construto utiliza o tipo herdeiro. Digamos que queremos uma lista à qual possa ser atribuída uma lista de objetos do tipo que não são ancestrais do objeto Dog . No entanto, não sabemos antecipadamente quais serão esses tipos específicos. Neste caso, uma construção da forma ? super Dog , para o qual todos os tipos são adequados - os progenitores da classe Dog :
List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals;
dogs.add(new Dog());
dogs.add(new Dog());
Podemos adicionar com segurança objetos do tipo Dog à lista com um , porque em qualquer caso ele possui todos os métodos implementados de qualquer um de seus ancestrais. Mas não poderemos adicionar um objeto do tipo Animal , pois não há certeza de que dentro dele haverá objetos desse tipo, e não, por exemplo, Dog . Afinal, podemos solicitar de um elemento desta lista um método da classe Dog , que Animal não terá . Neste caso, ocorrerá um erro de compilação. Além disso, se quiséssemos implementar o método anterior, mas com este genérico:
public static void animalsVoice(List<? super Dog> dogs) {
 for (Dog dog : dogs) {
   dog.voice();
 }
}
obteríamos um erro de compilação no loop for , pois não podemos ter certeza de que a lista retornada contém objetos do tipo Dog e estamos livres para usar seus métodos. Se chamarmos o método dogs.get(0) nesta lista . - obteremos um objeto do tipo Object . Ou seja, para que o método AnimalsVoice() funcione , precisamos pelo menos adicionar pequenas manipulações para restringir os dados do tipo:
public static void animalsVoice(List<? super Dog> dogs) {
 for (Object obj : dogs) {
   if (obj instanceof Dog) {
     Dog dog = (Dog) obj;
     dog.voice();
   }
 }
}
Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 15 - 7

16. Como existem métodos na classe Object?

Nesta parte da série, no parágrafo 11, já respondi a esta pergunta, por isso aconselho vivamente que a leia, caso ainda não o tenha feito. É aí que terminaremos por hoje. Vejo você na próxima parte! Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 15 - 8
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION