Sobrecarga e substituição de método em Java
*1. Visão geral *
Sobrecarga e substituição de método são conceitos-chave da linguagem de programação Java e, como tal, merecem uma análise aprofundada.
Neste artigo, aprenderemos o básico desses conceitos e veremos em que situações eles podem ser úteis.
===* 2. Sobrecarga de método *
*A sobrecarga de método é um mecanismo poderoso que nos permite definir APIs de classe coesas.* Para entender melhor por que a sobrecarga de método é um recurso tão valioso, vamos ver um exemplo simples.
Suponha que tenhamos escrito uma classe de utilitário ingênua que implementa métodos diferentes para multiplicar dois números, três números e assim por diante.
Se dermos aos métodos nomes enganosos ou ambíguos, como _multiply2 () _, _multiply3 () _, _multiply4 (), _, essa seria uma API de classe mal projetada. Aqui é onde a sobrecarga de método entra em jogo.
Simplificando, podemos implementar a sobrecarga de método de duas maneiras diferentes:
*implementando dois ou mais* métodos que têm o mesmo nome, mas recebem diferentes números de argumentos * *implementando dois ou mais* métodos que têm o mesmo nome, mas recebem argumentos de tipos diferentes *
====* 2.1 Números diferentes de argumentos *
A classe Multiplier mostra, em poucas palavras, como sobrecarregar o método _multiply () _ simplesmente definindo duas implementações que recebem diferentes números de argumentos:
public class Multiplier {
public int multiply(int a, int b) {
return a* b;
}
public int multiply(int a, int b, int c) {
return a *b* c;
}
}
2.2 Argumentos de diferentes tipos
Da mesma forma, podemos sobrecarregar o método _multiply () _ fazendo com que ele aceite argumentos de diferentes tipos:
public class Multiplier {
public int multiply(int a, int b) {
return a *b;
}
public double multiply(double a, double b) {
return a* b;
}
}
Além disso, é legítimo definir a classe Multiplier com os dois tipos de sobrecarga de método:
public class Multiplier {
public int multiply(int a, int b) {
return a *b;
}
public int multiply(int a, int b, int c) {
return a* b *c;
}
public double multiply(double a, double b) {
return a* b;
}
}
Vale ressaltar, no entanto, que não é possível ter duas implementações de método que diferem apenas em seus tipos de retorno .
Para entender o porquê - vamos considerar o seguinte exemplo:
public int multiply(int a, int b) {
return a *b;
}
public double multiply(int a, int b) {
return a* b;
}
Nesse caso, o código simplesmente não seria compilado devido à ambiguidade da chamada de método - o compilador não saberia qual implementação de _multiply () _ chamar.
2.3 Promoção de tipo
Um recurso interessante fornecido pela sobrecarga de método é a chamada promoção _type, a.k.a. [. ênfase] # ampliação da conversão primitiva # _.
Em termos simples, um determinado tipo é implicitamente promovido para outro quando não há correspondência entre os tipos de argumentos passados para o método sobrecarregado e uma implementação de método específico.
Para entender mais claramente como a promoção de tipos funciona, considere as seguintes implementações do método _multiply () _:
public double multiply(int a, long b) {
return a *b;
}
public int multiply(int a, int b, int c) {
return a* b * c;
}
Agora, chamar o método com dois argumentos int resultará na promoção do segundo argumento para long, pois nesse caso não há uma implementação correspondente do método com dois argumentos int.
Vamos ver um teste de unidade rápido para demonstrar a promoção de tipos:
@Test
public void whenCalledMultiplyAndNoMatching_thenTypePromotion() {
assertThat(multiplier.multiply(10, 10)).isEqualTo(100.0);
}
Por outro lado, se chamarmos o método com uma implementação correspondente, o tipo promotion simplesmente não ocorrerá:
@Test
public void whenCalledMultiplyAndMatching_thenNoTypePromotion() {
assertThat(multiplier.multiply(10, 10, 10)).isEqualTo(1000);
}
Aqui está um resumo das regras de promoção de tipo que se aplicam à sobrecarga de método:
-
byte pode ser promovido para short, int, long, float, _ ou _double
-
short pode ser promovido para int, long, float, _ ou _double
-
char pode ser promovido para int, long, float, _ ou _double
-
int pode ser promovido para long, float, _ ou _double
-
long pode ser promovido para float ou double *float pode ser promovido para double
====* 2.4. Ligação estática *
A capacidade de associar uma chamada de método específica ao corpo do método é conhecida como ligação.
No caso de sobrecarga de método, a ligação é realizada estaticamente em tempo de compilação, portanto, é chamada de ligação estática.
O compilador pode efetivamente definir a ligação no tempo de compilação, simplesmente verificando as assinaturas dos métodos.
===* 3. Substituição de método *
A substituição de método nos permite fornecer implementações refinadas em subclasses para métodos definidos em uma classe base.
Embora a substituição de método seja um recurso poderoso - considerando que é uma consequência lógica do uso de inheritance, um dos maiores pilares de https://en. wikipedia.org/wiki/Object-oriented_programming[OOP] -* quando e onde utilizá-lo deve ser analisado cuidadosamente, em casos de uso *.
Vamos ver agora como usar a substituição de método criando um relacionamento simples, baseado em herança ("é-a").
Aqui está a classe base:
public class Vehicle {
public String accelerate(long mph) {
return "The vehicle accelerates at : " + mph + " MPH.";
}
public String stop() {
return "The vehicle has stopped.";
}
public String run() {
return "The vehicle is running.";
}
}
E aqui está uma subclasse artificial:
public class Car extends Vehicle {
@Override
public String accelerate(long mph) {
return "The car accelerates at : " + mph + " MPH.";
}
}
Na hierarquia acima, simplesmente substituímos o método accelerate () _ para fornecer uma implementação mais refinada para o subtipo _Car.
Aqui, é claro que se um aplicativo usa instâncias da classe Vehicle, também pode funcionar com instâncias de Car , pois as duas implementações do método _accelerate () _ têm a mesma assinatura e o mesmo tipo de retorno.
Vamos escrever alguns testes de unidade para verificar as classes Vehicle e Car:
@Test
public void whenCalledAccelerate_thenOneAssertion() {
assertThat(vehicle.accelerate(100))
.isEqualTo("The vehicle accelerates at : 100 MPH.");
}
@Test
public void whenCalledRun_thenOneAssertion() {
assertThat(vehicle.run())
.isEqualTo("The vehicle is running.");
}
@Test
public void whenCalledStop_thenOneAssertion() {
assertThat(vehicle.stop())
.isEqualTo("The vehicle has stopped.");
}
@Test
public void whenCalledAccelerate_thenOneAssertion() {
assertThat(car.accelerate(80))
.isEqualTo("The car accelerates at : 80 MPH.");
}
@Test
public void whenCalledRun_thenOneAssertion() {
assertThat(car.run())
.isEqualTo("The vehicle is running.");
}
@Test
public void whenCalledStop_thenOneAssertion() {
assertThat(car.stop())
.isEqualTo("The vehicle has stopped.");
}
Agora, vamos ver alguns testes de unidade que mostram como os métodos run () _ e _stop () _, que não são substituídos, retornam valores iguais para _Car e Vehicle:
@Test
public void givenVehicleCarInstances_whenCalledRun_thenEqual() {
assertThat(vehicle.run()).isEqualTo(car.run());
}
@Test
public void givenVehicleCarInstances_whenCalledStop_thenEqual() {
assertThat(vehicle.stop()).isEqualTo(car.stop());
}
No nosso caso, temos acesso ao código fonte das duas classes, para que possamos ver claramente que a chamada do método accelerate () _ em uma instância _Vehicle base e a chamada accelerate () _ em uma instância _Car retornarão valores diferentes para a mesma argumento.
Portanto, o teste a seguir demonstra que o método substituído é chamado para uma instância de Car:
@Test
public void whenCalledAccelerateWithSameArgument_thenNotEqual() {
assertThat(vehicle.accelerate(100))
.isNotEqualTo(car.accelerate(100));
}
3.1 Substituibilidade do tipo
Um princípio central no OOP é o da substituibilidade de tipo, que está intimamente associada ao Liskov Substitution Principium (LSP).
Simplificando, o LSP declara que se um aplicativo funciona com um determinado tipo de base, também deve funcionar com qualquer um dos seus subtipos . Dessa forma, a substituibilidade do tipo é preservada adequadamente.
-
O maior problema com a substituição de método é que algumas implementações de método específicas nas classes derivadas podem não aderir totalmente ao LSP e, portanto, falham em preservar a substituibilidade de tipo.
Obviamente, é válido criar um método substituído para aceitar argumentos de tipos diferentes e retornar um tipo diferente também, mas com total aderência a essas regras:
-
Se um método na classe base usa argumentos de um determinado tipo, o método substituído deve usar o mesmo tipo ou um supertipo (também conhecido como argumentos do método contravariant)
-
Se um método na classe base retornar void, o método substituído deverá retornar void
-
Se um método na classe base retornar uma primitiva, o método substituído deverá retornar a mesma primitiva
-
Se um método na classe base retornar um determinado tipo, o método substituído deverá retornar o mesmo tipo ou um subtipo (também conhecido como tipo de retorno covariant) *Se um método na classe base lança uma exceção, o método substituído deve lançar a mesma exceção ou um subtipo da exceção da classe base
====* 3.2 Ligação dinâmica *
Considerando que a substituição de método pode ser implementada apenas com herança, onde existe uma hierarquia de um tipo e subtipo (s) de base, o compilador não pode determinar em tempo de compilação qual método chamar, pois a classe base e as subclasses definem o mesmos métodos.
Como conseqüência, o compilador precisa verificar o tipo de objeto para saber qual método deve ser chamado.
Como essa verificação ocorre no tempo de execução, a substituição do método é um exemplo típico de ligação dinâmica.
===* 4. Conclusão*
Neste tutorial, aprendemos como implementar a sobrecarga e a substituição de métodos e exploramos algumas situações típicas em que são úteis.
Como sempre, todos os exemplos de código mostrados neste artigo estão disponíveis over no GitHub.