Java Sobrecarga

Em Java, dois métodos da mesma classe podem ter o mesmo nome, desde que recebam parâmetros diferentes. Isso se chama sobrecarga (em inglês, overloading). Você já usa sem perceber: o System.out.println aceita números, textos, letras e true/false porque existem várias versões dele, todas com o mesmo nome.

Você vai aprender
  • Como declarar vários métodos com o mesmo nome e listas de parâmetros diferentes.
  • O que é a assinatura de um método e por que ela decide tudo.
  • Como o Java escolhe qual versão chamar, inclusive quando o argumento é int e só existe a versão double.
  • O que não é sobrecarga: mudar só o tipo de retorno ou só o nome do parâmetro.
  • Os erros reais already defined, reference … is ambiguous e no suitable method found.

Antes, revise Parâmetros e Argumentos e Valores de Retorno: a sobrecarga é toda sobre listas de parâmetros.


A ideia antes do código: o verbo "abrir"

Em português, você "abre" uma porta, "abre" um pote, "abre" um arquivo e "abre" uma conta no banco. É o mesmo verbo, mas a ação muda conforme o que vem depois. Ninguém inventa "abrirPorta", "abrirPote" e "abrirConta" na conversa: o objeto da frase já deixa claro qual "abrir" é.

A sobrecarga é isso no Java: um nome só (somar) para a mesma ideia, e o Java descobre qual versão usar pelo que você passa entre os parênteses.


O problema: nomes diferentes para a mesma coisa

A Ana quer somar dois inteiros, três inteiros e dois números com vírgula. Sem sobrecarga, ela teria de inventar um nome para cada caso: somarDois, somarTres, somarDecimais… e lembrar qual é qual. Com sobrecarga, todos se chamam somar:

public class Main {
    static int somar(int a, int b) {
        return a + b;
    }

    static int somar(int a, int b, int c) {
        return a + b + c;
    }

    static double somar(double a, double b) {
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(somar(2, 3));
        System.out.println(somar(2, 3, 4));
        System.out.println(somar(2.5, 3.5));
    }
}
$ javac Main.java && java Main
5          « somar(int, int)
9          « somar(int, int, int)
6.0        « somar(double, double): o resultado é double
somar(int a, int b)
Primeira versão: dois int.
somar(int a, int b, int c)
Segunda versão: a quantidade de parâmetros é diferente (três).
somar(double a, double b)
Terceira versão: a quantidade é igual à da primeira, mas os tipos são diferentes.
somar(2, 3)
Na chamada, o compilador olha os argumentos (dois int) e escolhe a versão que combina. A escolha é feita na compilação, não durante a execução.

A assinatura do método

O que o Java usa para diferenciar métodos é a assinatura: o nome mais os tipos dos parâmetros, na ordem. As três versões acima têm assinaturas diferentes:

DeclaraçãoAssinaturaFaz parte da assinatura?
static int somar(int a, int b)somar(int, int)Sim: nome, quantidade, tipos e ordem dos parâmetros.
Não: o tipo de retorno, os nomes dos parâmetros, o static.
static int somar(int a, int b, int c)somar(int, int, int)
static double somar(double a, double b)somar(double, double)

Regra de ouro: numa classe, não podem existir dois métodos com a mesma assinatura. Para sobrecarregar, a lista de parâmetros tem de mudar em pelo menos um destes pontos: quantidade, tipo ou ordem dos tipos.

Missão: já existem maior(int, int) e maior(double, double). Crie uma terceira versão, maior(int a, int b, int c), que devolva o maior dos três. Dica: ela pode usar a versão de dois: return maior(maior(a, b), c);.

public class Main {
    static int maior(int a, int b) {
        return a > b ? a : b;
    }

    static double maior(double a, double b) {
        return a > b ? a : b;
    }

    public static void main(String[] args) {
        System.out.println(maior(7, 15, 3));
        System.out.println(maior(8.5, 9.0));
    }
}
public class Main {
    static int maior(int a, int b) {
        return a > b ? a : b;
    }

    static int maior(int a, int b, int c) {
        return maior(maior(a, b), c);
    }

    static double maior(double a, double b) {
        return a > b ? a : b;
    }

    public static void main(String[] args) {
        System.out.println(maior(7, 15, 3));
        System.out.println(maior(8.5, 9.0));
    }
}

Como o Java escolhe a versão

Quando há várias versões, o compilador procura a que combina melhor com os argumentos. Veja um experimento com uma versão int e uma double:

public class Main {
    static void mostrar(int x) {
        System.out.println("versão int: " + x);
    }

    static void mostrar(double x) {
        System.out.println("versão double: " + x);
    }

    public static void main(String[] args) {
        mostrar(5);
        mostrar(5.0);
        mostrar(5L);
        mostrar('A');
        mostrar(2.5f);
    }
}
$ javac Main.java && java Main
versão int: 5          « 5 é int: combinação exata
versão double: 5.0     « 5.0 é double: combinação exata
versão double: 5.0     « 5L é long: não cabe em int sem perda, mas cabe em double
versão int: 65         « 'A' é char: o char vira int (o código da letra A é 65)
versão double: 2.5     « 2.5f é float: vira double

O raciocínio do compilador, simplificado, é este:

  1. Existe uma versão com os tipos exatos? Use essa. (mostrar(5) → int; mostrar(5.0) → double.)
  2. Não? Existe uma versão em que os argumentos cabem por conversão automática, sem perda? Essa conversão se chama ampliação (widening): byte → short → int → long → float → double, e char → int. Se várias servem, use a mais "próxima": por isso o 'A' foi para int e não para double.
  3. Ainda não? O Java tenta converter primitivos em objetos (int → Integer, veja Wrappers) e, por último, versões com varargs.
  4. Nada serviu? Erro: no suitable method found. Duas serviram igualmente bem? Erro: ambiguous.

Brinque com o código: rode e confira as cinco linhas. Depois acrescente uma versão static void mostrar(char x) e veja qual chamada muda de versão. Por fim, apague a versão int: para onde vão o 5 e o 'A'?

public class Main {
    static void mostrar(int x) {
        System.out.println("versão int: " + x);
    }

    static void mostrar(double x) {
        System.out.println("versão double: " + x);
    }

    public static void main(String[] args) {
        mostrar(5);
        mostrar(5.0);
        mostrar(5L);
        mostrar('A');
        mostrar(2.5f);
    }
}

O caminho contrário (estreitamento) nunca é automático: um double não entra num parâmetro int. Agora veja o que acontece quando só existe a versão double:

public class Main {
    static void mostrar(double x) {
        System.out.println("versão double: " + x);
    }

    public static void main(String[] args) {
        mostrar(5);
        mostrar(5.0);
    }
}
$ javac Main.java && java Main
versão double: 5.0     « o 5 (int) foi ampliado para 5.0
versão double: 5.0

Sem a versão int, o 5 é convertido para double e entra na única que existe. É a mesma conversão que você viu em Conversão de Tipos.

A ordem dos tipos também conta

Mudar só a ordem dos tipos já cria uma assinatura nova:

public class Main {
    static void cadastrar(int idade, double altura) {
        System.out.println("idade e altura");
    }

    static void cadastrar(double altura, int idade) {
        System.out.println("altura e idade");
    }

    public static void main(String[] args) {
        cadastrar(30, 1.65);
        cadastrar(1.65, 30);
    }
}
$ javac Main.java && java Main
idade e altura
altura e idade

Funciona, mas é uma armadilha: basta alguém chamar cadastrar(30, 2) (dois int) para o compilador não saber qual escolher. Você verá o erro em Quando dá errado. Em código real, evite sobrecargas que diferem só na ordem.


Você já usa sobrecarga: o println

O System.out.println tem uma versão para cada tipo: println(int), println(double), println(char), println(boolean), println(String), println(char[]), println(Object) e println() sem nada. Cada uma sabe escrever o seu tipo:

public class Main {
    public static void main(String[] args) {
        System.out.println(42);
        System.out.println(3.14);
        System.out.println('A');
        System.out.println(true);
        System.out.println("texto");
        char[] letras = {'o', 'i'};
        System.out.println(letras);
    }
}
$ javac Main.java && java Main
42
3.14
A
true
texto
oi          « println(char[]) escreve as letras do array

Outros exemplos da biblioteca do Java: Math.max e Math.abs (versões para int, long, float, double; veja Math) e texto.indexOf, que aceita um caractere ou um texto (veja Métodos de String).


Formas diferentes: sobrecarga para "valores padrão"

Algumas linguagens deixam escrever saudar(nome = "visitante"), com um valor padrão para o parâmetro. Java não tem isso. O jeito Java é a sobrecarga: as versões curtas chamam a versão completa, preenchendo o que falta.

public class Main {
    static void saudar() {
        saudar("visitante");
    }

    static void saudar(String nome) {
        saudar(nome, "Bom dia");
    }

    static void saudar(String nome, String cumprimento) {
        System.out.println(cumprimento + ", " + nome + "!");
    }

    public static void main(String[] args) {
        saudar();
        saudar("Ana");
        saudar("Bruno", "Boa noite");
    }
}
$ javac Main.java && java Main
Bom dia, visitante!     « saudar() → saudar("visitante") → saudar("visitante", "Bom dia")
Bom dia, Ana!
Boa noite, Bruno!

Só a última versão tem o println. Se a Ana quiser mudar o formato da saudação, muda num lugar só. Você vai ver a mesma técnica com construtores, em Construtores (this(…)).

Qual usar?

SituaçãoUse
A mesma ideia com tipos diferentes (somar para int e double)Sobrecarga por tipo
A mesma ideia com mais ou menos informações (saudar(), saudar(nome))Sobrecarga por quantidade, com as versões curtas chamando a completa
Uma quantidade qualquer de valores do mesmo tipoVarargs (int... numeros), em vez de várias versões
Ações realmente diferentesNomes diferentes: abrirConta e fecharConta, não dois conta(…)
Sobrecarga não é sobrescrita

Os nomes parecem, mas são coisas diferentes. Sobrecarga (overloading, esta aula): mesmo nome, parâmetros diferentes, na mesma classe. Sobrescrita (overriding): uma classe filha substitui um método herdado, com a mesma assinatura; é o assunto de Sobrescrita (@Override).


O que não é sobrecarga

Mudar só o tipo de retorno

A Ana quis duas médias: uma inteira e uma com vírgula, ambas com dois int:

public class Main {
    static int media(int a, int b) {
        return (a + b) / 2;
    }

    static double media(int a, int b) {
        return (a + b) / 2.0;
    }

    public static void main(String[] args) {
        System.out.println(media(7, 8));
    }
}
$ javac Main.java
Main.java:6: error: method media(int,int) is already defined in class Main
    static double media(int a, int b) {
                  ^
1 error

"O método media(int,int) já está definido na classe Main". Repare que a mensagem mostra a assinatura, media(int,int), sem o tipo de retorno: para o Java, os dois métodos são o mesmo. E faz sentido: na chamada media(7, 8) não há nada que diga qual dos dois você quer, já que o retorno só aparece depois da chamada (e pode até ser ignorado).

Como resolver: use nomes diferentes (mediaInteira e media) ou mantenha só a versão double.

Mudar só o nome do parâmetro

$ javac Main.java
Main.java:6: error: method saudar(String) is already defined in class Main
    static void saudar(String apelido) {
                ^
1 error

saudar(String nome) e saudar(String apelido) têm a mesma assinatura, saudar(String). O nome do parâmetro só existe dentro do método; quem chama não o vê.


Quando dá errado

"method … is already defined in class …"

O que aconteceu: dois métodos com a mesma assinatura. Ou você mudou só o tipo de retorno, só o nome do parâmetro, ou colou o método duas vezes.

Como resolver: mude a lista de parâmetros (quantidade, tipo ou ordem), use outro nome ou apague a cópia.

"reference to … is ambiguous"
$ javac Main.java
Main.java:13: error: reference to cadastrar is ambiguous
        cadastrar(30, 2);
        ^
  both method cadastrar(int,double) in Main and method cadastrar(double,int) in Main match
1 error

O que aconteceu: "a referência a cadastrar é ambígua: os dois métodos combinam". Com cadastrar(30, 2), o 30 serve para a primeira versão (como int) e o 2 serve virando double; na segunda versão, é o contrário. Nenhuma é melhor que a outra, e o compilador se recusa a sortear.

Como resolver: deixe a chamada exata (cadastrar(30, 2.0)) ou, melhor, elimine as sobrecargas que só trocam a ordem dos tipos.

"no suitable method found for …"
$ javac Main.java
Main.java:11: error: no suitable method found for mostrar(String)
        mostrar("cinco");
        ^
    method Main.mostrar(int) is not applicable
      (argument mismatch; String cannot be converted to int)
    method Main.mostrar(double) is not applicable
      (argument mismatch; String cannot be converted to double)
1 error

O que aconteceu: "nenhum método adequado para mostrar(String)". Quando há sobrecarga, o javac lista cada versão que tentou e o motivo de cada uma não servir: a versão int não aceita texto, a versão double também não.

Como resolver: mande um argumento de um tipo aceito (mostrar(5)) ou crie a versão que falta: static void mostrar(String x). Com uma versão só, o erro seria o mais curto incompatible types (veja Parâmetros).

Foi chamada a versão "errada"
$ java Main.java
versão int: 65

O que aconteceu: não é erro do Java. A Ana chamou mostrar('A') esperando ver a letra, mas não existe mostrar(char); o char foi ampliado para int (o código 65).

Como resolver: crie a versão para o tipo que você quer tratar de forma especial (mostrar(char x)) ou converta o argumento de propósito na chamada.


Referência

Pode coexistir com void f(int a, double b)?RespostaPor quê
void f(int a)SimQuantidade diferente
void f(int a, int b)SimTipo diferente
void f(double a, int b)Sim (mas cuidado: chamadas ambíguas)Ordem dos tipos diferente
int f(int a, double b)Não: already definedSó o retorno mudou
void f(int x, double y)Não: already definedSó os nomes mudaram
void f(int... a)SimVarargs é int[], outro tipo

Missão final: área com sobrecarga

Missão: crie duas versões de area: area(double lado), que devolve a área do quadrado (lado × lado), e area(double largura, double altura), que devolve a do retângulo. O main já chama as duas.

public class Main {
    // escreva as duas versões de area aqui

    public static void main(String[] args) {
        System.out.println("Quadrado: " + area(4));
        System.out.println("Retângulo: " + area(3, 5));
    }
}
public class Main {
    static double area(double lado) {
        return lado * lado;
    }

    static double area(double largura, double altura) {
        return largura * altura;
    }

    public static void main(String[] args) {
        System.out.println("Quadrado: " + area(4));
        System.out.println("Retângulo: " + area(3, 5));
    }
}

Exercícios

1. A assinatura

Escreva a assinatura (nome e tipos, sem espaços) de static String juntar(String a, int vezes):

2. Qual versão?

Existem f(int x) e f(double x). Qual tipo de parâmetro recebe a chamada f(10L)?

3. Complete a sobrecarga

Complete a versão sem parâmetros para que ela chame a versão com nome, usando "visitante":

static void saudar() { }

4. Qual par de métodos não compila na mesma classe?

5. Existem mostrar(int) e mostrar(double). O que mostrar('B') faz?

6. Quando o Java escolhe qual versão sobrecarregada chamar?


Resumo

  • Sobrecarga: vários métodos com o mesmo nome e listas de parâmetros diferentes (quantidade, tipo ou ordem dos tipos).
  • A assinatura é o nome + os tipos dos parâmetros. Não entram o retorno nem os nomes dos parâmetros.
  • O compilador escolhe a versão pelos tipos dos argumentos: primeiro a exata, depois por ampliação (int → long → double, char → int), depois wrappers e varargs.
  • Só o retorno diferente não é sobrecarga: method … is already defined.
  • Duas versões igualmente boas: reference … is ambiguous. Nenhuma serve: no suitable method found, com a lista de tentativas.
  • Java não tem parâmetros com valor padrão; use versões curtas que chamam a completa.