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 14

Publicado no grupo Random-PT
Fogo de artifício! O mundo está em constante movimento e nós estamos em constante movimento. Anteriormente, para se tornar um desenvolvedor Java, bastava conhecer um pouco da sintaxe Java e o resto viria. Com o tempo, o nível de conhecimento necessário para se tornar um desenvolvedor Java cresceu significativamente, assim como a concorrência, que continua a empurrar para cima o nível inferior de conhecimento necessário. Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 1Se você realmente deseja se tornar um desenvolvedor, você precisa ter isso como certo e se preparar completamente para se destacar entre iniciantes como você. O que faremos hoje, ou seja, continuaremos analisando as mais de 250 questões . Em artigos anteriores, examinamos todas as questões de nível júnior e hoje abordaremos questões de nível médio. Embora eu observe que essas questões não são 100% de nível médio, você pode encontrar a maioria delas em uma entrevista de nível júnior, porque é nessas entrevistas que ocorre uma análise detalhada de sua base teórica, enquanto para um aluno do ensino médio o as perguntas são mais focadas em sondar sua experiência. Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 2Mas, sem mais delongas, vamos começar.

Meio

São comuns

1. Quais são as vantagens e desvantagens da OOP quando comparada à programação processual/funcional?

Tinha essa pergunta na análise das perguntas do Juinior e por isso eu já respondi. Procure esta pergunta e sua resposta nesta parte do artigo, questões 16 e 17.

2. Como a agregação difere da composição?

Na POO, existem vários tipos de interação entre objetos, unidos sob o conceito geral de “Relacionamento Tem-A”. Esse relacionamento indica que um objeto é um componente de outro objeto. Ao mesmo tempo, existem dois subtipos deste relacionamento: Composição - um objeto cria outro objeto e o tempo de vida de outro objeto depende do tempo de vida do criador. Agregação - um objeto recebe um link (ponteiro) para outro objeto durante o processo de construção (neste caso, o tempo de vida do outro objeto não depende do tempo de vida do criador). Para melhor compreensão, vejamos um exemplo específico. Temos uma determinada classe de carro - Car , que por sua vez possui campos internos do tipo - Engine e uma lista de passageiros - List<Passenger> , também possui um método para iniciar o movimento - startMoving() :
public class Car {

 private Engine engine;
 private List<Passenger> passengers;

 public Car(final List<Passenger> passengers) {
   this.engine = new Engine();
   this.passengers = passengers;
 }

 public void addPassenger(Passenger passenger) {
   passengers.add(passenger);
 }

 public void removePassengerByIndex(Long index) {
   passengers.remove(index);
 }

 public void startMoving() {
   engine.start();
   System.out.println("Машина начала своё движение");
   for (Passenger passenger : passengers) {
     System.out.println("В машине есть пассажир - " + passenger.getName());
   }
 }
}
Neste caso, a Composição é a ligação entre Car e Engine , já que o desempenho do carro depende diretamente da presença do objeto motor, pois se engine = null , então receberemos um NullPointerException . Por sua vez, um motor não pode existir sem uma máquina (por que precisamos de um motor sem máquina?) e não pode pertencer a várias máquinas ao mesmo tempo. Isso significa que se excluirmos o objeto Car , não haverá mais referências ao objeto Engine e em breve ele será excluído pelo Garbage Collector . Como você pode ver, essa relação é muito rígida (forte). Agregação é a ligação entre Carro e Passageiro , já que o desempenho de Carro não depende de forma alguma de objetos do tipo Passageiro e sua quantidade. Eles podem sair do carro - removePassengerByIndex(Long index) ou inserir novos - addPassenger(Passenger passageiro) , apesar disso, o carro continuará funcionando corretamente. Por sua vez, os objetos Passenger podem existir sem um objeto Car . Como você entende, esta é uma conexão muito mais fraca do que vemos na composição. Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 3Mas isso não é tudo, um objeto que está conectado por agregação a outro também pode ter uma determinada conexão com outros objetos ao mesmo tempo. Por exemplo, você, como estudante de Java, está matriculado em cursos de inglês, OOP e logaritmos ao mesmo tempo, mas ao mesmo tempo não é uma parte criticamente necessária deles, sem a qual o funcionamento normal é impossível (como como um professor).

3. Quais padrões GoF você usou na prática? Dar exemplos.

Já respondi essa pergunta antes, então vou deixar apenas um link para a análise , veja a primeira pergunta. Também encontrei um maravilhoso artigo com dicas sobre padrões de design, que recomendo fortemente manter em mãos.

4. O que é um objeto proxy? Dar exemplos

Um proxy é um padrão de design estrutural que permite substituir objetos substitutos especiais, ou em outras palavras, objetos proxy, em vez de objetos reais. Esses objetos proxy interceptam chamadas para o objeto original, permitindo que alguma lógica seja inserida antes ou depois da chamada ser passada para o original. Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 4Exemplos de uso de um objeto proxy:
  • Como proxy remoto - usado quando precisamos de um objeto remoto (um objeto em um espaço de endereço diferente) que precisa ser representado localmente. Neste caso, o proxy cuidará da criação, codificação, decodificação da conexão, etc., enquanto o cliente a utilizará como se fosse o objeto original localizado no espaço local.

  • Como proxy virtual - usado quando um objeto que consome muitos recursos é necessário. Nesse caso, o objeto proxy serve como algo como uma imagem de um objeto real que na verdade ainda não existe. Quando uma solicitação real (chamada de método) é enviada para este objeto, só então o objeto original é carregado e o método é executado. Essa abordagem também é chamada de inicialização lenta; isso pode ser muito conveniente, porque em algumas situações o objeto original pode não ser útil e então não haverá custo para criá-lo.

  • Como proxy de segurança - usado quando você precisa controlar o acesso a algum objeto com base nos direitos do cliente. Ou seja, se um cliente sem direitos de acesso tentar acessar o objeto original, o proxy irá interceptá-lo e não permitir.

Vejamos um exemplo de proxy virtual: Temos uma interface de manipulação:
public interface Processor {
 void process();
}
cuja implementação utiliza muitos recursos, mas ao mesmo tempo pode não ser utilizada sempre que o aplicativo for iniciado:
public class HiperDifficultProcessor implements Processor {
 @Override
 public void process() {
   // некоторый сверхсложная обработка данных
 }
}
Classe proxy:
public class HiperDifficultProcessorProxy implements Processor {
private HiperDifficultProcessor processor;

 @Override
 public void process() {
   if (processor == null) {
     processor = new HiperDifficultProcessor();
   }
   processor.process();
 }
}
Vamos executá-lo em main :
Processor processor = new HiperDifficultProcessorProxy();
// тут тяжеловсеного оригинального an object, ещё не сущетсвует
// но при этом есть an object, который его представляет и у которого можно вызывать его методы
processor.process(); // лишь теперь, an object оригинал был создан
Observo que muitas estruturas usam proxy e, para o Spring , esse é um padrão chave (o Spring é costurado por dentro e por fora). Leia mais sobre esse padrão aqui . Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 5

5. Quais inovações foram anunciadas no Java 8?

As inovações trazidas pelo Java 8 são as seguintes:
  • Interfaces funcionais foram adicionadas, leia sobre que tipo de animal é esse aqui .

  • Expressões lambda, que estão intimamente relacionadas às interfaces funcionais, leia mais sobre seu uso aqui .

  • Adicionada API Stream para processamento conveniente de coletas de dados, leia mais aqui .

  • Adicionados links para métodos .

  • O método forEach() foi adicionado à interface Iterable .

  • Adicionada uma nova API de data e hora no pacote java.time , análise detalhada aqui .

  • API simultânea aprimorada .

  • Adicionando uma classe wrapper Opcional , que é usada para lidar corretamente com valores nulos, você pode encontrar um excelente artigo sobre este tópico aqui .

  • Adicionando a capacidade das interfaces de usar métodos estáticos e padrão (o que, em essência, aproxima o Java da herança múltipla), mais detalhes aqui .

  • Adicionados novos métodos à classe Collection(removeIf(), spliterator()) .

  • Pequenas melhorias no Java Core.

Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 6

6. O que é Alta Coesão e Baixo Acoplamento? Dar exemplos.

Alta Coesão ou Alta Coesão é o conceito quando uma determinada classe contém elementos intimamente relacionados entre si e combinados para sua finalidade. Por exemplo, todos os métodos da classe User devem representar o comportamento do usuário. Uma classe terá baixa coesão se contiver elementos não relacionados. Por exemplo, a classe User contendo um método de validação de endereço de e-mail:
public class User {
private String name;
private String email;

 public String getName() {
   return this.name;
 }

 public void setName(final String name) {
   this.name = name;
 }

 public String getEmail() {
   return this.email;
 }

 public void setEmail(final String email) {
   this.email = email;
 }

 public boolean isValidEmail() {
   // некоторая логика валидации емейла
 }
}
A classe user pode ser responsável por armazenar o endereço de email do usuário, mas não por validá-lo ou enviar o email. Portanto, para obter alta coerência, movemos o método de validação para uma classe de utilitário separada:
public class EmailUtil {
 public static boolean isValidEmail(String email) {
   // некоторая логика валидации емейла
 }
}
E usamos conforme necessário (por exemplo, antes de salvar o usuário). Low Coupling ou Low Coupling é um conceito que descreve a baixa interdependência entre módulos de software. Essencialmente, a interdependência é como mudar um requer mudar o outro. Duas classes têm um acoplamento forte (ou acoplamento forte) se estiverem intimamente relacionadas. Por exemplo, duas classes concretas que armazenam referências entre si e chamam os métodos uma da outra. Classes fracamente acopladas são mais fáceis de desenvolver e manter. Por serem independentes entre si, podem ser desenvolvidos e testados em paralelo. Além disso, eles podem ser alterados e atualizados sem afetar um ao outro. Vejamos um exemplo de classes fortemente acopladas. Temos algumas turmas de alunos:
public class Student {
 private Long id;
 private String name;
 private List<Lesson> lesson;
}
Que contém uma lista de lições:
public class Lesson {
 private Long id;
 private String name;
 private List<Student> students;
}
Cada lição contém um link para os alunos participantes. Aperto incrivelmente forte, não acha? Como você pode reduzi-lo? Primeiro, vamos garantir que os alunos não tenham uma lista de disciplinas, mas uma lista de seus identificadores:
public class Student {
 private Long id;
 private String name;
 private List<Long> lessonIds;
}
Em segundo lugar, a turma da aula não precisa saber sobre todos os alunos, então vamos deletar completamente a lista deles:
public class Lesson {
 private Long id;
 private String name;
}
Então ficou muito mais fácil e a conexão ficou bem mais fraca, não acham? Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 7

POO

7. Como você pode implementar herança múltipla em Java?

Herança múltipla é um recurso do conceito orientado a objetos onde uma classe pode herdar propriedades de mais de uma classe pai. O problema surge quando existem métodos com a mesma assinatura tanto na superclasse quanto na subclasse. Ao chamar um método, o compilador não pode determinar qual método de classe deve ser chamado e mesmo ao chamar o método de classe que tem precedência. Portanto, Java não suporta herança múltipla! Mas existe uma espécie de brecha, da qual falaremos a seguir. Como mencionei anteriormente, com o lançamento do Java 8, a capacidade de ter métodos padrão foi adicionada às interfaces . Se a classe que implementa a interface não substituir este método, então esta implementação padrão será usada (não é necessário substituir o método padrão, como implementar um abstrato). Neste caso, é possível implementar diferentes interfaces em uma classe e utilizar seus métodos padrão. Vejamos um exemplo. Temos uma interface de flyer, com um método fly() padrão :
public interface Flyer {
 default void fly() {
   System.out.println("Я лечу!!!");
 }
}
A interface do walker, com o método walk() padrão :
public interface Walker {
 default void walk() {
   System.out.println("Я хожу!!!");
 }
}
A interface do nadador, com o método swim() :
public interface Swimmer {
 default void swim() {
   System.out.println("Я плыву!!!");
 }
}
Bem, agora vamos implementar tudo isso em uma classe duck:
public class Duck implements Flyer, Swimmer, Walker {
}
E vamos executar todos os métodos do nosso pato:
Duck donald = new Duck();
donald.walk();
donald.fly();
donald.swim();
No console receberemos:
Eu vou!!! Estou voando!!! Eu estou nadando!!!
Isso significa que representamos corretamente a herança múltipla, embora não seja o que é. Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 8Observarei também que se uma classe implementa interfaces com métodos padrão que possuem os mesmos nomes de métodos e os mesmos argumentos nesses métodos, o compilador começará a reclamar de incompatibilidade, pois não entende qual método realmente precisa ser usado. Existem várias saídas:
  • Renomeie métodos em interfaces para que sejam diferentes entre si.
  • Substitua esses métodos controversos na classe de implementação.
  • Herde de uma classe que implementa esses métodos controversos (então sua classe usará exatamente sua implementação).

8. Qual é a diferença entre os métodos final, finalmente e finalize()?

final é uma palavra-chave usada para colocar uma restrição em uma classe, método ou variável, uma restrição que significa:
  • Para uma variável - após a inicialização, a variável não pode ser redefinida.
  • Para um método, o método não pode ser substituído em uma subclasse (classe sucessora).
  • Para uma classe - a classe não pode ser herdada.
finalmente é uma palavra-chave antes de um bloco de código, usada ao lidar com exceções, em conjunto com um bloco try e junto (ou de forma intercambiável) com um bloco catch. O código neste bloco é executado em qualquer caso, independentemente de uma exceção ser lançada ou não. Nesta parte do artigo, na questão 104, são discutidas situações excepcionais em que este bloqueio não será executado. finalize() é um método da classe Object , chamado antes de cada objeto ser excluído pelo coletor de lixo, este método será chamado (último), e é usado para limpar recursos ocupados. Para obter mais informações sobre os métodos da classe Object que todo objeto herda, consulte a pergunta 11 nesta parte do artigo. Bem, é aí que terminaremos hoje. Vejo você na próxima parte! Análise de perguntas e respostas de entrevistas para desenvolvedor Java.  Parte 14 - 9
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION