Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Certificação Java SE 7 Programmer I: relações entre classes

Certificação Java SE 7 Programmer I: relações entre classes

Implementando Herança - Introdução

Em Java podemos nós implementar a herança, é isso que vamos ver neste capítulo! O primeiro passo da herança é ter alguma especialização de uma classe, isso é o que fazemos com a herança de classes. Eu tenho uma superclasse e uma subclasse, eu tenho uma classe mãe e uma classe Filha, ou uma classe pai e uma classe Filha. Superclasse e subclasse.

A herança entre classes permite que o código da classe mãe (da superclasse, da classe pai) possa ser herdado e reaproveitado pela classe Filha (pela subclasse). Essa é só uma das maneiras de reaproveitamento de código que existem na linguagem. Agora, essa maneira é através da herança de classes. Vamos vê-la com mais cuidado e detalhes.

Um exemplo de classe mãe seria simplesmente eu criar um arquivo “TestaHeranca.java”. Vou criar uma classe mãe. A minha classe mãe é uma classe normal porque eu pego e coloco ali. Se a classe Filha tem que herdar da classe mãe, ela tem que herdar da superclasse, ela tem que especializar a minha classe genérica (mais genérica, especializando). Digitei: class Mae { e na outra linha } class Filha extends Mae {.

Em Java nós falamos que a classe herda da outra extends. Digitei: class Neta extends Filha { }. Nessa classe Neta estende Filha, que estende Mae. Isto é, Neta herda de Filha que herda de Mae e indiretamente Neta também herda de Mae.

Lembrando que toda classe em Java herda de object, eu vou mostrar um exemplo. Se eu escrevo a minha classe aqui, Guilherme, que é uma classe muito especial, ela herda de extends object porque toda classe que nós não escrevemos extends alguma coisa, o compilador vai adicionar o extends object.

A classe mãe por padrão já vai estender object. Se a classe mãe por padrão, assim como a classe Guilherme, já estende object Filha e Neta, indiretamente também elas herdam de object, por isso todas as classes em Java herdam de object.

Uma regra importante: não podemos herdar de duas classes. A classe Neta não pode herdar de Filha e Guilherme, não compila. Vamos tentar compilar?

Não, eu estava esperando um “abre chaves”, mas não tem “abre chaves”, só posso herdar de uma classe. Cuidado com a pegadinha, se eu escrever: class Mae extends Guilherme, object, também não compila. Você não pode herdar de duas classes, você herda indiretamente. Você herda de uma e essa uma herda de outra, que herda de outra e que alguém lá em cima herda de object. Não pode!

É claro que se eu vou herdar de uma classe mãe ou de alguma outra classe, eu tenho que conseguir acessar essa outra classe; porque se estou escrevendo que a classe Mae herda de Guilherme, a classe Guilherme tem que ser acessível aqui nesse escopo.

Por exemplo: se eu tenho uma classe em outro pacote, como no pacote modelo, eu tenho a classe Endereco. Agora eu vou fazer a minha classe: class Avenida extends Endereco { }. A classe Endereco está no pacote modelo, por isso: class Avenida extends modelo.Endereco { }. Vou tentar compilar.

Legal, estou herdando endereço! Agora eu tenho que, além de herdar uma classe que é acessível ao meu pacote, do instante e do lugar que estou, eu tenho que também ter o construtor dela acessível. Qual o construtor padrão da classe Endereco? Endereco() { } e o construtor padrão da classe avenida é: Avenida() { }.

Tem um segredo: todo construtor padrão, por padrão, chama o construtor da classe pai, construtor sem argumentos da classe pai. O construtor sem argumentos da classe pai é esse construtor aqui, é o construtor sem argumentos da classe object.

Compilamos isso e ele fala que o construtor não é público. Sim, o construtor do endereço que eu criei só pode ser acessado nesse pacote. Esse construtor até está herdando, mas não está no mesmo pacote. Além da classe ser acessível, o construtor dela também ter que ser acessível. Pelo menos um construtor tem que ser acessível, para eu poder acessar.

Já que é o construtor sem argumentos, se eu colocar public...

Beleza! Se eu colocar protected...

Beleza, porque eu estou herdando! Agora, se eu tirar daqui da avenida o meu construtor, o Java adiciona esse código para nós. Lembra? Esse é o código padrão. Está OK, está chamando o construtor protected daquele código.

Agora, e se esse meu construtor aqui do endereço recebesse uma (String rua)? Não existe mais o construtor padrão. Quando eu tentar chamar o super, ele não acha e aí ele fala: "não existe construtor sem argumentos". A mesma coisa, se eu tirar esse código daqui. É o código padrão, o compilador adiciona. Não existe construtor sem argumentos.

A solução que eu tenho é colocar o meu construtor sem argumentos de forma que seja acessível, aí sim eu compilo. As duas regras de prioridade em uma classe são: primeiro, a classe tem que ser acessível. Segundo, eu tenho que ter um construtor acessível.

Por padrão, o construtor que acessamos é o construtor sem argumentos. Se eu quisesse chamar um construtor com argumentos, chamaria: super("Avenida");. Não tem problema. Só que, nesse caso eu tenho que escrever explicitamente a chamada para esse construtor, não posso deixar o padrão. O padrão vai chamar o sem argumentos.

Além disso, a classe que estou herdando não pode ser final, uma classe final não pode ser herdada. Eu vou tentar compilar. A classe final não pode ser herdada. Exemplo de classe final importante: string. String é final, eu tento herdar dela e não herdo porque é uma classe final.

"Você pode argumentar: “Guilherme, eu já sei quais são as regras para poder herdar de uma classe: não pode ser final, tem que ter um construtor acessível e ela tem que ser acessível. Agora, o que eu ganho? Que história é essa de herdar as coisas da minha classe pai, da minha classe mãe e da minha superclasse?" A sacada é que você estaria herdando os métodos e atributos da sua classe pai e mãe.

Vou criar a minha classe X: class X {, que tem um int x;, que tem um public void y() {, que é um método. Depois tem system.out.printin("Invocando y em um objeto");. Agora eu vou ter uma classe y que herda de X: class Y extends X {}. É importantíssimo: class TestaHeranca { e na linha debaixo public static void main(String[] args) {}.

Dentro desse método main eu vou criar e acessar esse meu objeto do tipo Y: Y y = new Y();. Olhe aqui a memória, o que está acontecendo? Criando um objeto na memória do tipo Y. Y herda de X, é como se dentro desse objeto do tipo Y eu tivesse também um espaço com as coisas do tipo X. Não são dois objetos diferentes na memória, um único objeto na memória.

Esse objeto tem espaço para variável X, que é o int. Tem um método chamado Y e esse tipo desse objeto aqui é o tipo Y, que também é um X e que também é um object. Sem problemas, mas é um objeto só que eu criei. Agora, crio uma variável Y aqui no meu escopo local, ele referencia esse meu objeto.

Agora fala: y.x = 15; ey.y();. Repare que na certificação nós vamos pegar sempre com esses nomes feios, porque a certificação tenta te pegar em um descuido. Eu posso não perceber que eu tenho uma variável com o mesmo nome, desde que seja minúsculo, não tem problema nenhum. Se tivesse o mesmo nome maiúsculo, teria que tomar outros cuidados porque a certificação vai tentar nos pegar nisso.

Neste caso, eu estou falando que a classe Filha também ganhou o int e também ganhou um método que eu tinha lá, sempre se atentando ao escopo. Se essa variável int x fosse privada, não poderia acessá-la. Vamos testar!

Compilo e testo digitando java TestaHeranca... Está invocando o y em um objeto. Claro, tudo isso depende da visibilidade da variável do método. Se a variável e o método fossem privados, esses dois objetos não poderiam ser acessados por fora. Aqui dentro da minha classe que testa a herança, não tem como acessar eles mesmo que eles existam na classe Y, em um objeto do tipo Y. Vamos testar!

Compilo... Fala que X e Y são privados e eu não posso acessá-los. Vou tirar agora o private para deixar compilando. Está compilando. Agora eu quero saber como essa herança funciona com a variável membro, com o método, com método estático e método de classe.

Como é que funciona a herança com o método estático? Não existe herança de um método estático, o método estático não é herdado, mas você pode referenciá-lo; porque quando você herda, você consegue referenciar os métodos estáticos mais facilmente.

Vamos dar esse exemplo? Vou criar uma nova classe: class W {, que vai ter um public static void metodo() { }. Uma class Z extends W { }. Aqui dentro do meu “TesteHeranca.java” eu posso a qualquer instante chamar W.metodo();, posso também chamar Z.metodo();. Os dois estão referenciando o mesmo método e tempo de compilação.

Vou colocar um system.out.printin("invocando o metodo estatico que foi definido em W");. Foi definido em W, mas estou referenciando ele tanto através de W quanto através de Z. Vamos testar! Compilo e rodo. Fala que invocando, o método estático foi definido em W duas vezes.

E se eu tentar herdar? Vou tentar herdar esse método e sobrescrevê-lo. Vou colocá-lo aqui de novo:"Invocando método estático foi definido em Z". Será que ele invoca? Vou tentar, javac... Compilou, rodou imprimindo W e Z. Porque o bind de um método estático é feito em compilação. Se é feito em compilação, em compilação ele olhou W.metodo e Z.metodo e não houve herança de método.

Você pode me perguntar: "como você sabe que não houve, Guilherme?" Tente chamar aqui super.metodo();. Quem é super? O super de um objeto do tipo Z é o W. Verdade, só que isso aqui não é um objeto do tipo Z, isso aqui é um static.

Será que compila isso aqui? Isso aqui nem compila! Apareceu: “error : non-static variable super cannot be referenced from a...”. O super é uma variável não estática, você não pode acessar. Não tem como escrever super dentro de um método estático, é por isso que esse código nem compila.

Eu posso definir um novo método que tenha o mesmo nome e assinatura, que referencia ou um, ou outro. Se esse método não existir, sempre vai chamar o código da minha classe pai. Quer dizer, apesar de eu herdar esses métodos, no sentido que eu consigo acessá-los, eu não herdei no sentido de conseguir trocá-los. Eu consigo definir outro método totalmente diferente que não tem nada haver com o primeiro.

Veja como é perigoso fazer isso na prática, porque se o código está fazendo de uma maneira e passa a chamar de outra, pode acontecer algumas bagunças. A regra geral é definir o método estático em uma classe. Herdou? Você pode referenciá-lo através de qualquer uma das duas classes.

Se você redefinir esse método na classe de baixo, na classe Filha, o que vai acontecer é que se você acessar em tempo de compilação de uma referência para o tipo de cima, estará tudo bem. Se for para o tipo de baixo, debaixo. Vai olhar em tempo de compilação.

Outra maneira de ver que não existe essa herança é que não se pode escrever um método estático que seja abstract, ele requer que seja sobrescrito e definido em uma classe filha.

Mas se esse método é abstrato e estático, ele teria que ser escrito na classe filha? Mas a classe filha não tem nada haver o escopo estático dela e com o escopo estático meu. É por isso que não dá, ele não compila. Eu tento compilar e não pode, abstract static não faz sentido.

Apesar de eu ter falado que estou sobrescrevendo esse método, não é sobrescrita, é só uma maneira de dizer. O que estamos fazendo aqui não é sobrescrito. Tem gente que fala que você está redefinindo esse método, que você está recriando esse método só nesse escopo estático da classe Z e esse só do escopo estático da classe W. Claro que se ele não existisse aqui, também poderia ter referenciado ele através da palavra Z ou W.

Estou batendo na tecla de que o baind da invocação do método é feito em tempo de compilação, não em tempo de execução. Você pode me perguntar: "o que isso quer dizer mesmo, Guilherme?"

Quer dizer que se eu estiver dois objetos, isso é, se eu criar um objeto do tipo Z e um objeto do tipo W, se eu criar um objeto do tipo Z z = new Z(); e chamar esse método Z.metodo();, ele vai chamar baseado na referência que eu utilizei em tempo de compilação e essa referência é do tipo Z. Ele vai me impor invocando o método estático que foi definido em Z.

Vamos testar? Compilo e rodo. A última mensagem avisa que está invocando método estático e foi definido em Z. Agora, e se eu tenho uma variável do tipo W? Vamos ver a variável do tipo W: W.metodo();. Vou tentar e agora a última mensagem deve ser avisando que está invocando o método do tipo W.

Lembre-se que eu posso chamar os métodos estáticos baseados na variável. Agora, e se esse método aqui for na verdade um objeto do tipo Z? Através de herança, por causa do polimorfismo eu posso chamar um objeto do tipo Z de W. Eu estou chamando ele de W aqui e estou chamando o método estático. Se o método não fosse estático, isso seria conferido em tempo de execução e ele iria invocar um método em W.

Porém, se esse método não fosse estático, isso seria conferido em tempo de execução, em binding, e aí ele iria usar o do objeto do tipo Z. Como isso daqui é feito em compilação, o método estático teve o binding feito em compilação. Vamos olhar o método que ele vai invocar. O W é do tipo W em tempo de compilação. Se W é do tipo W em tempo de compilação, esse é o método que é bindiado.

Não importa que em tempo de execução o objeto seja do tipo Z, para métodos estáticos o binding é feito em tempo de compilação, portanto não importa o objeto que esteja referenciando em tempo de execução. Quando eu chamar o método, ele vai chamar no método que eu tinha compilado. Isso é, ele vai chamar em W. Eu compilo e ele chama em W.

Da mesma maneira que falamos disso tudo, para construtor também não existe a herança porque você não herda o construtor da sua classe pai, você invoca o construtor da sua classe pai através da palavra super. Como falamos, todo objeto acaba herdando indiretamente de object, ou diretamente.

Quer dizer que todo mundo tem todos os métodos que estão definidos em object. Se todo mundo tem todos os métodos que estão definidos em object, o que eu posso fazer é chamar esses métodos que foram definidos em object. Por exemplo: como é o método muito famoso que existe em object, que é o método que vamos ter cobrado aqui, o método toString.

O método toString pega um objeto e devolve uma versão string deste objeto, que serve para nos ajudar a entender o que está acontecendo no nosso programa. Essa é a utilização adequada dele. Por exemplo: se eu tiver uma classe aqui chamada class Gato e um gato tiver uma string, que a cor dos olhos seria String corDosOlhos, o que eu posso fazer é criar esse Gato g = new Gato();.

E falar que, por exemplo, a minha cor dos olhos não tem nada haver com esse gato, a cor dos olhos é mel, essa é cor dos meus olhos. Digitei: g.corDosOlhos = "mel";. A cor do nosso gato é mel também, uma coincidência! E esse gato eu vou dar agora um System.out println(g.toString());. O método que existe em todo objeto que usamos bastante, que é toString.

Vai transformar esse objeto em um string para nós entendermos melhor o que é esse objeto que estamos trabalhando nessa linha de código - debug, alguma informação etc. Vou compilar e vou rodar. Apareceu Gato@5d9d277e. Ajudou bastante.

Nos ajudou, sabemos que ele é um gato. O método toString, por padrão, nos devolve o tipo do objeto em tempo de execução. Se ele é um gato é um gato mesmo, que não é um tipo de referência em compilação, um ponteiro, um endereço da memória do nosso objeto. Esse valor para nós acaba sendo não utilizável aqui nesse contexto, mas é isso que ele devolve, por padrão.

É legal sobrescrevemos ele, é bem comum sobrescrevemos esse método. Não só nos preocuparmos com as regras de sobrescrita e de método, que não é cobrado nessa prova, mas eu vou sobrescrever e retornar um Gato com essa cor dos olhos: return "[Gato " + corDosOlhos + "]";.

Vou imprimir e testar. Compilei e rodei. Chamou o método toString, ele imprimiu [Gato mel]. O legal do toString é que toda vez que usamos um objeto no contexto de uma string o toString costuma ser chamado automaticamente.

Por exemplo: o System.out.println já tem uma versão que, se você passar um objeto, ele chama o toString para você. Vamos ver... Compilei e rodei.

Tem outra versão também que se você tenta concatenar valores "Estou com um: ", quando você concatena um valor com um object ele chama o método toString. Vamos testar. Compilo e rodo. Estou com um [Gato mel].

O toString nos mostra que estamos sempre herdando de object mesmo, ganhando a implementação padrão no método toString sempre. Esse método é chamado quando damos um println em um objeto, quando concatenamos.

Em vários momentos em que queiramos usar um objeto como uma string, chamamos o método toString dele. O importante aqui é se lembrar que toda classe Java herda de object, só não herda diretamente se você herdar de outra classe. Isso quer dizer que a sua classe mãe, sua superclasse, é uma outra.

Porém, a sua super mãe ou a sua superclasse herda de object. Se ela não herda de object, uma outra classe ali em cima vai herdar de object. Se você tentar fechar isso no loop, não vai compilar. Quando você herda, você herdou tudo o que é membro, você também tem e você consegue acessar de acordo com as regras de visibilidade.

Aquilo que era estático você também pode acessar agora, de acordo com as regras de visibilidade. Você pode definir um método estático na classe Filha, que tem o mesmo nome do método estático na classe Pai, mas é um método totalmente diferente. Se você chamar uma referência com o Filho, será o Filho e se chamar uma referência com o Pai, será o Pai.

O binding de um método estático é feito em tempo de compilação, isso é, se você compilou chamando o método estático através de W da classe Pai, da superclasse, da classe Mae, ele vai chamar a superclasse da classe Mae. Se você escreveu Filho, ele vai chamar no Filho. Depende de quem você chamou em compilação, esse é o escopo que temos.

A classe object que estamos herdando tem o método toString que estamos usando aqui e sobrescrevendo ele public String toString, que retorna uma string.

Desenvolva código que mostra o uso de polimorfismo - Polimorfismo

Essa sessão vai nos cobrar para mostrarmos um código que se beneficia do uso de polimorfismo.

O conceito de reescrita de um método significa herdar uma classe e sobrescrever; reescrever o comportamento de um método de uma classe, na minha classe pai e mãe. Vamos lá?

Por exemplo: eu vou criar uma classe: class TestaReescrita { }. O nome vai ser TestaReescrita.java. Na classe TestaReescrita tem: public static void main(String[] args) { }, o meu arquivo vai ter a minha classe pai ou mãe, chamada Veiculo: class Veiculo { }. A classe Veiculo tem a capacidade, um método de ser ligado: public void Liga() { }. O método é implementado: System.out.println("O veiculo esta sendo ligado!");.

Vou ter um tipo de Veiculo que vou chamar de Helicoptero: class Helicoptero { }. Já sabemos que no momento em que eu herdar de Veiculo - com uma classe estendendo e herdando de outra, class Helicoptero extends Veiculo { } - eu tenho que saber que se eu criar o meu Helicoptero h1 = new Helicoptero();, ele herdará o comportamento, os métodos da classe pai/mãe: h1.liga();.

Vamos testar? Esse código é um código de herança que nós já conhecemos. Eu vou ao terminal, compilar o meu TestaReescrita.java. Eu rodo: TestaReescrita e ele imprime: OVeiculoestá sendo ligado!

O uso de herança, sobrescrita, reescrita do meu método ou sobrescrever o meu método, significa que na minha classe filha eu vou ter esse método de novo definido: public void liga();.

Repare que a assinatura é a mesma, o método se chama liga, recebe a mesma quantidade de argumentos e é isso aí. Se ele tem o mesmo nome e a mesma quantidade e tipos de argumentos, eu estou reescrevendo ele - se ele já existia no meu pai. Não posso colocar dois iguais na mesma classe.

É claro, não posso fazer a sobrecarga de métodos com os mesmos parâmetros, mesmo nome, mas se um está na classe pai e o outro está na classe filha e eles forem iguais em relação ao nome do método, tipos e quantidade de parâmetros, aí sim, estarei fazendo a reescrita.

Sobrecarga é quando tenho dois métodos com o mesmo nome e valores diferentes, com parâmetros diferentes. Sobrescrita é quando tenho o mesmo nome, mesmos tipos e quantidade de parâmetros em classes distintas em que uma herda da outra.

Estou sobrescrevendo: System.out.println("Ligando o helicoptero");. Vamos ligar o Helicoptero. Eu salvo, compilo e rodo. Agora ele está ligando o Helicoptero. O que aconteceu foi que eu criei um objeto do tipo Helicoptero, eu tenho um espaço de memória na minha memória, que é um objeto do tipo Helicoptero.

Eu tenho uma variável que é um outro espaço chamado h1, que referencia esse objeto. Eu cheguei lá no meu objeto na memória e chamei o método liga. Em tempo de execução, não é compilação, em tempo de execução dinamicamente o Java vai olhar qual é o tipo desse objeto, vai ver se tem o método liga com os parâmetros que eu quero e vai chamar o método dele.

Se não tivesse, ele iria olhar na classe pai, na classe avô, na classe bisavó etc. Como já tem logo de cara na classe filha, ele chama esse método. É por isso que nós sobrescrevemos porque não demos a chance do interpretador da virtual machine procurar nas classes pai, mãe, avó, bisavó etc.

Logo de cara, nós sobrescrevemos esse método e fizemos com que a virtual machine chamasse ele. Porque a virtual machine vai procurar um método em tempo de execução, o binding é feito em tempo de execução.

Eu vou mudar um pouco o meu código, ao invés de eu declarar uma variável do tipo Helicoptero, vou declarar uma variável do tipo Veiculo. Declarei a variável do tipo Veiculo e agora o que tenho aqui é o polimorfismo tradicional. Um objeto do tipo Helicoptero é um objeto do tipo Veiculo: Veiculo h1 = new Helicoptero();.

Em tempo de compilação o compilador vai olhar e se perguntar: “existe método liga em variáveis do tipo Veiculo?” Falamos de compilação ainda. A resposta é que existe! Todo Veiculo tem um método do tipo liga, isso é compilação. Compilou o assunto e morreu.

Agora nós vamos para execução, voltamos para a memória. Lembra da memória? Olhe o desenho aqui da memória novamente.

Eu tenho a variável h1 referenciando um objeto do tipo Helicoptero. A variável é do tipo Veiculo, mas ela referência um objeto do tipo Helicoptero. Ali atrás eu falei que em tempos de execução o Java vai olhar o tipo de objeto dinamicamente.

Em tempo de execução, quando eu chamar o método liga para valer, nessa hora o interpretador do Java vai olhar aqui na memória e vai perguntar qual é o tipo desse objeto. É Helicoptero. O Helicoptero tem método liga? Tem método liga. Chame o método liga.

Aí ele chama o método liga, o executa e imprime ligando o Helicoptero. Vamos testar? Compilo, rodo e ele imprime ligando o Helicoptero. Era o que eu queria! Repare que o polimorfismo faz com que eu consiga tratar todos os Veiculo de uma maneira só através de Veiculo e que eu possa chamar todos os métodos que Veiculo tem independentemente de saber o que vai ser executado de verdade.

Se ele é um Helicoptero, vai ser executado uma coisa; se ele é um Veiculo, vai ser executado outra. Não importa, estou mandando ligar. O que ele faz, comportamento de como ele implementa o método, eu não estou preocupado. Eu estou preocupado na interface. O método liga existe e eu posso chamar esse método, que ele vai ligar.

Como ele faz isso lá dentro é uma preocupação da classe, não minha. Está controlando o escopo do comportamento da minha classe e eu estou controlando o escopo, estou me protegendo na minha classe.

Para reescrever o método existem algumas regras que eu tenho que seguir. Vamos às regras! A primeira delas é: o nome do método tem que ser exatamente o mesmo. Cuidado com letras maiúsculas e minúsculas, cuidado com a pegadinha no nome do método, o nome tem que ser exatamente o mesmo.

Claro, o método tem que ser definido em uma classe filha ou neta. Se está sendo definido em uma classe que não tem nada haver com aquela minha classe, não é reescrita, é só um método com o mesmo nome e classes diferentes. Tem que ser em uma classe filha, neta etc.

Tem que ter exatamente o mesmo nome, a mesma quantidade, ordem e tipo de parâmetros. Têm que ser exatamente os mesmos, aí é reescrita. Têm alguns detalhes: no momento que estou reescrevendo o método eu tenho que cuidar de algumas coisas.

Se a visibilidade do meu método pai, do meu método lá em cima, é uma a visibilidade do meu filho, tem que ser pelo menos igual à do pai ou mais aberta. Porque imagine que o pai escreve uma classe com um método público e aí o filho escreve que esse método é privado.

Vamos colocar esse exemplo aqui, eu vou aqui no meu Helicoptero. Imagine que na classe pai eu tenho um método público e na filha eu tenho um privado. Antes eu podia acessar esse método e agora eu não posso mais, o compilador iria ficar maluco.

A outra regra é: a visibilidade tem que ser igual ou mais aberta do que a mãe. As exceptions lançadas têm que ser iguais às da mãe, ou menos. Você não pode de repente definir que vai lançar mais exceptions.

Você não pode fazer alguma coisa que a sua mãe falou que ela não iria fazer. Se ela falou que ela só jogava runtime exception, você não pode falar que você vai jogar io exception, porque não herda de runtime exception então não pode falar.

Você só pode jogar as exceptions que a sua mãe, seu pai, avô etc. definiram. O método da mãe não pode ser final. Se o método na classe mãe, avó etc. for final, você não pode sobrescrever aquele método.

As regras são: têm ser os mesmos nomes, os mesmo tipos de parâmetros, quantidades e tipo, aí você estará fazendo uma sobrescrita. Se você está fazendo sobrescrito o retorno do método, a primeira regra é que tem que ser igual ao mais específico.

A segunda regra é que a visibilidade tem que ser igual ou mais aberta. Trouxe? Tem que ser igual ou menos. Também, o método do pai ou da mãe não podem ser finais, senão você não pode herdar, é a regra do final.

Se essas regras forem respeitadas, compile. Vamos ver alguns dos erros de compilação dessas regras. Vou pegar esse método do Helicoptero e vou colocá-lo aqui como privado. Meu método agora é privado: private void liga();. Antes era público e agora é privado. Vou tentar compilar.

Não compila, não pode sobrescrever falando que ele está mais fechado. Não pode ser mais fechado porque imagine se eu tivesse essa variável como Helicoptero, eu não poderia invocar esse método porque o método é privado, mas se eu recebesse ele como Veiculo, eu poderia porque o método é público. Dependendo da minha referência eu posso ou não posso acessar esse método.

Para não cair nesse caso bizarro o Java definiu que ele não pode sobrescrever definindo uma visibilidade menor. A visibilidade sempre tem que ser igual ou maior. Se está na classe pública sempre vai ter que ser público.

E o throws? Imagine que o meu Veiculo possa jogar um IOExcepction, que é um Java io.IOException - public void liga() throws java.io.IOException { }. Se eu tentar compilar, não vai porque no método main eu preciso decidir o que fazer com esse IOException.

Eu compilo e está compilando porque o método da classe filha pode jogar as mesmas exceptions ou suas filhas etc., ou menos, nunca será mais. Por exemplo: se eu falar que eu quero que esse método jogue agora java.sql.SQLException {. O SQLException não herda de IOException, o SQLException é uma outra não tem nada haver com IOException.

Você está falando que se você chamar esse método através da interface, através da classe do tipo Helicoptero ou uma referência para Helicoptero, você vai ter que dar um try catch de SQLException.

Se você tiver a variável através de uma referência Veiculo, você vai ter que dar um try catch de IOException. Decida-se, você não pode ser instável assim! Se eu tenho uma variável com uma referência, eu tenho que dar um try catch. Se eu tenho com outra referência, outro try catch.

Não! Tem que ser sempre o mesmo try catch. Se eu tentar isso daqui, não vai compilar, o meu throws tem que ser sempre mais específico ou igual. Eu não posso fazer isso. Posso fazer: throws java.io.FileNotFoundException porque FileNotFoundException herda de IOException. Posso fazer, não tem problema nenhum.

Seguindo essas regras, eu ainda tenho alguns detalhes que eu tenho que tomar cuidado. Por exemplo: no caso de uma interface que tem um método void x();, todo método de uma interface é por padrão público e abstrato. Se você não colocou body nele, ele é público e abstrato.

Então a minha classe B é que implementa A? Cuidado, o seu método x não pode ser package default, não pode ser só void x(). Vamos tentar compilar... Não compilou porque tem que ser público, todo método de interface por padrão é público. Você pode argumentar: "Guilherme, vou colocá-lo aqui como protected". Não pode! Todo método de interface é público, quer você queira ou não.

Repare que eu tenho aqui as minhas classes de Veiculo, vou criar um outro Veiculo e vou criar aqui um Droid: class Droid extends Veiculo { }, mas têm vários tipos de Droid e eu não quero ainda definir como ele funciona. Eu vou colocar um método abstrato: abstract void liga();. O meu método é abstrato, não está definido.

Se ele é abstrato, eu vou colocar que a classe é abstrata. Repare que um método que já foi definido em uma classe pai pode ser redefinido na classe filha, pode ser reescrito na classe filha como abstrato.

Parece estranho, mas eu posso reescrevê-lo como abstrato porque isso quer dizer que eu tenho que colocar como público. Compila. Só que quando eu quiser criar um tipo de Droid - por exemplo: um HexaDroid, que tem seis gira giras em cima dele - neste caso, se eu tentar salvá-lo, ele vai reclamar porque eu não escrevi o método liga porque vai funcionar como um método abstrato normal

Eu preciso escrever esse método: public void liga() { e na outra linha System.out.println("hexa");. Eu posso sobrescrever um método concreto com um método abstrato, não tem problema. Eu só tenho que seguir as outras regras que eu esqueci de seguir. Tenho que seguir a regra do modificador de acesso: no mínimo, igual ou mais aberto.

Como funciona no caso do retorno? O retorno funciona da seguinte maneira, por exemplo: eu tenho uma class FabricaDeVeiculo {, que tem um método que fabrica veiculo: veiculo fabrica() { return new Veiculo();.

Eu vou sobrescrever essa classe: class FabricaDeHexaDroid {, porque ela é genial, e a FabricaDeHexaDroid tem um fabrica, só que ela fabrica HexaDroids: veiculo fabrica { return new HexaDroid();. Ela herda de FabricaDeVeiculo: class FabricaDeHexaDroid extends FabricaDeVeiculo {

Repare que quando eu herdei e sobrescrevi o método, eu coloquei o mesmo tipo de retorno. Se eu colocasse um outro tipo de retorno totalmente diferente não iria compilar. Vamos testar primeiro esse. Vou tentar compilar... Compilou!

Se eu colocasse um tipo de retorno nada haver como int fabrica() { return 2, por exemplo, que não tem nada haver, não compilaria. Precisa ser um tipo de retorno compatível com Veiculo. Quando estou sobrescrevendo o tipo tem que ser compatível.

Só que quando eu usá-lo, repare que vai ficar um pouco estranho: veiculo h1 = new FabricaDeVeiculo().fabrica();. Fabricou um Veiculo. Agora, se ao invés de fabricar um Veiculo, eu quero fabricar um HexaDroid: veiculo h1 = new FabricaDeHexaDroid().fabrica();. Fabricou um HexaDroid.

Agora eu queria que HexaDroid já devolvesse logo de cara um HexaDroid, eu queria poder escrever isso daqui. Afinal, a fábrica de HexaDroid devolve HexaDroids. Eu queria compilar isso aqui e não compila porque a definição do método diz que está devolvendo um Veiculo e não um HexaDroid.

Eu gostaria que nesse método eu devolvesse um HexaDroid: HexaDroid fabrica(). O tipo de retorno na sobrescrita tem que ser compatível. Isso significa que tem ser igual a HexaDroid ou mais específico.

O HexaDroid é um Veiculo. Quando a sua classe filha devolve um HexaDroid, ela está devolvendo um Veiculo, não está mentindo. Ela está assumindo os mesmos compromissos que a classe pai. A classe mãe tinha assumido.

É essa regra que você tem que usar durante a prova e o seu dia a dia de sobrescrita. Você tem que comprovar que você assumiu os mesmos compromissos que a sua classe pai e que você não está avacalhando.

Por exemplo: como eu avacalho o compromisso da minha classe pai? A classe pai falou que ia devolver um determinado tipo e eu estou devolvendo um tipo totalmente diferente. Eu posso devolver aquele mesmo tipo ou algum mais especifico, porque também é daquele tipo. Como eu avacalhei? Eu avacalhei se eu jogasse no exception que o meu pai nunca falou que ia jogar. Lembrando que todo mundo joga, por padrão, o RuntimeExCeption.

Como eu avacalhei? Eu avacalharia se o meu pai falasse que aquele método tem um tipo de acesso e de repente eu dissesse que não, que agora é mais fechado. Essas regras eu tenho que seguir, eu tenho que tomar esse cuidado. Porém, o retorno pode ser aquele ou um mais específico. O throws pode ser aquele ou mais específico e o acesso pode ser aquele ou um mais aberto.

"Legal, Gui, eu vi que eu consigo herdar e eu consigo sobrescrever o método e quando eu tenho um objeto não importa se estou referenciando ele através de um Veiculo ou se estou referenciando ele através daquele tipo específico e em tempo de execução, ele vai pegar um método de instância”.

“Vai fazer o lookup dinamicamente, vai encontrar quem é o tipo que implementou aquele método que sobrescreveu ali na herança e vai usar aquele método. Não vai usar o do pai ou do avô etc., ele vai procurar primeiro a implementação do filho para cima”.

"Agora como é que isso funciona quando eu invoco um método?" Por exemplo: imagine que eu tenho um método aqui que se chama: static void metodo(Veiculo v) {} e recebe um Veiculo.

Se eu recebo um Veiculo e chamo o v.Liga();, se eu chamar esse método passando para ele um HexaDroid, vai compilar? Digitei metodo(new FabricaDeHexaDroid().fabrica();.

Compila, é claro que compila! HexaDroid é um Veiculo e eu posso chamar esse método. Estou passando um tipo mais específico, que é um Veiculo. Se eu chamar esse método passando um Veiculo, vai compilar? Digitei metodo(new Veiculo());.

Compila! Com o mesmo método eu consigo receber dois tipos diferentes de objetos porque um é o outro, essa é a verdade. Eu consigo passar um Helicoptero? Digitei metodo(new Helicoptero());.

Consigo! Consigo passar qualquer tipo de Veiculo para esse método. Eu tenho um método polimórfico. O método polimórfico é um método que recebe um tipo e todos os filhos daquele tipo, ele recebe todo mundo que é daquele tipo e ele não sabe qual está recebendo.

Ele invoca o método despreocupado de qual ele está invocando porque não importa para quem invoca ou como é feita a implementação, o que importa é a responsabilidade que aquele método tem. O método vai ligar aquele Veiculo. Estou feliz em como ele liga, não importa como.

A interface de comunicação é o que me importa; o resto é encapsulado, o comportamento é encapsulado. O método polimórfico é o aquele que vai chamar aqui alguma coisa desse tipo de objeto sem eu saber qual é o tipo específico.

A verdade é que eu não estou nem aí, quero que ele me passe o que quiser de Veiculo. Eu vou chamar o método liga porque todo Veiculo tem o método liga e em tempo de execução ele vai me dizer qual deles está invocando.

Vamos tentar compilar? Eu vou lá e compilo. Esqueci do IOException! Vou tentar novamente. Vou rodar e eu tenho: hexa, O veiculo está sendo ligado! Ligando o helicoptero. Ele chamou o método liga sempre em tempo de execução dinamicamente, método de instância público nessa situação ou package protected. Em tempo de execução ele faz o lookup, a busca dinâmica desse método, encontra o método e invoca para nós.

A regra geral é que o compilador nunca sabe o valor das nossas variáveis, em geral. Têm aqueles casos de algumas literais que ele até conhece, mas em geral ele não sabe o valor das nossas variáveis, ele não sabe a classe que ele vai invocar em tempo de compilação. Se eu tenho a herança e eu estou trabalhando com sobrescrita de método, o que acontece quando eu chamo this ou super para invocar o método do meu pai ou o meu método?

Vamos fazer alguns testes? Vou criar aqui algumas classes, algumas classes brincalhonas para nós. Vou criar uma primeira classe chamada A, ela vai ter um primeiro método que imprime A, um método bem simples: class A{, na outra linha public void metodo() { e na outra System.out.println("a");.

E eu tenho a classe B que estende de A, tem um método que imprime B e ela chama também o método no meu super. Isso é, ela fala: "pegue a minha classe pai, avô etc., procure nela um método e chame nela esse método." Ficou: class B extends A {, na próxima linha public void metodo() {, na outra linha System.out.println("b"); e na outra super,metodo();.

Se eu tentar compilar esse código, eu conseguirei. Se eu tentar chamar super. e um método que não existe, como super.metodo23474046789;, é claro que em tempo de compilação ele vai perceber esse erro, porque o método não existe e não pode chamar. Tenho que chamar um método super que exista do meu pai, do meu avô e etc. lá para cima na minha herança.

Vou colocar um outro método: public void metodo2() {, na outra linha System.out.println("c"); e vou imprimir aqui também a chamada para o método e a chamada para o super.metodo: metodo(); super.metodo();. Vamos testar? Vou compilar.

Compila! Agora vou criar um método de teste na classe B mesmo: public static void main(String[] args) { e na outra linha new B().metodo2();. Vou chamar o metodo2. Vou rodar a minha classe B... Apareceu: CBAA Porque CBAA? Vamos ver?

Quando eu chamei o metodo2 ele chamou o metodo2, imprimiu C. Chamou o método do meu próprio objeto e o método do meu próprio objeto é o método, dinamicamente. Vamos lá na memória!

Olhe a memória aqui em cima, nela eu tenho um objeto do tipo B e estou chamando nesse objeto o metodo2 que imprimiu a letra C. Agora eu chamo o método no próprio objeto. Ele faz o lookup dinâmico para mim.

Quem ele encontrou? O próprio método da classe B, ele vai imprimir agora a letra B. Só que dentro do método B, que imprime a letra B. Ele chama o super que imprime a letra A. Por enquanto, C B A.

Terminou o método, terminou o método do B, voltou para o metodo2 e agora ele chama super.metodo. Posso chamar super.metodo? Pode! Quando o chama, ele chama o super, não chama o do atual. Encontra ali e imprime A de novo.

E se eu fizer uma pequena mudança aqui? Se eu fizer com que o método aqui na minha classe A chame o meu método this.metodo2(); e eu escreva aqui o metodo2, que simplesmente imprime metodo2 do pai?

Você pode falar: "acalma-se aí, Gui! O que você está falando é que é para chamar o metodo2 no próprio objeto". Vamos testar? Compilei e rodei. Entrou em looping. Agora você me perguntar: “Guilherme, quando você chamou this.metodo2 o que aconteceu? Você não chamou o metodo2 no próprio A?"

Não! Eu chamei o metodo2 no objeto. O objeto é do tipo B, aí ele procura o metodo2 no objeto do tipo B e encontra ele aqui. Aí ele chama esse aqui, aí ele chama o super.metodo, aqui, aí ele procura o this.metodo2 no objeto, encontra no objeto o metodo2 e entra em looping.

Seria a mesma coisa que não colocar o this*. Cuidado porque o this pode te enganar, pode parecer que você está chamando nessa classe! Não, ele está chamando nesse objeto. Com ou sem this, se você só chama o método, ele está chamando o método nesse objeto e entra em looping. Temos que tomar cuidado com esse tipo de looping na herança.

Diferencie tipo de uma referência e tipo de um objeto - Referência e Objeto

Nessa sessão nós vamos ser cobrados a diferenciarmos o tipo de uma referência do tipo de um objeto. Na verdade, quando falamos de herança e de polimorfismo, já estávamos diferenciando isso: o objeto que eu tenho na minha memória que só em tempo de execução e eu sei quem é esse objeto versus a referência que eu tenho dentro de um método, dentro de algum lugar que estou acessando uma referência.

Esse método referencia um objeto, mas quem é esse objeto eu só sei em tempo de execução. Nós temos que diferenciar esses dois momentos de vida do nosso programa: o tempo de compilação, onde eu tenho referências, e o tempo de execução, onde eu tenho a referência apontando para um objeto na memória.

Vamos diferenciar entre o tipo de uma referência e o tipo de um objeto. Vamos lá: class TestaTipoDeReferenciaEObjeto { public static main(String[] args) { }. Aqui dentro o que eu vou ter será dois tipos, duas classes que eu quero ter no meu sistema, que são a Conta e a ContaJuridica: class Conta { } e class ContaJuridica { }.

A ContaJuridica herda de conta: class ContaJuridica extends Conta { }. A qualquer momento eu posso falar: Conta c = new ContaJuridica();. A mesma coisa com as listas do Java, por exemplo: import Java.util.*;. Nós temos o ArrayList, eu posso fazer aqui: List lista = new ArrayList();.

Nos dois casos eu estou usando o polimorfismo para dizer que essa minha ArrayList eu vou referenciar como se fosse uma lista. Essa minha ContaJuridica eu vou referenciar como se fosse uma Conta. Nos dois casos são usados o polimorfismo para referenciar um objeto através de uma variável.

Vamos para a memória! Lá na memória eu tenho que criar um objeto do tipo ContaJuridica. Crio uma variável do tipo Conta e mando essa variável do tipo Conta referenciar o objeto do tipo ContaJuridica: c ----> ContaJuridica.

Não tem problema, a ContaJuridica é uma conta, eu posso referenciar. Crio um objeto do tipo ArrayList, crio uma variável do tipo lista e mando a variável de referências com essa minha seta aquele objeto do ArrayList: lista ----> ArrayList.

Vou colocar alguma coisa na minha conta, vou falar que a minha conta tem um ID: int id. Vou falar que a ContaJuridica tem um string CNPJ: String cnpj;. Agora eu vou pegar essa minha Conta e vou falar que o ID dela é 15: c.id = 15;. Eu vou falar que o CNPJ dele é 5000, porque eu não sei nenhum CNPJ de cabeça: c.cnpj = "5000";.

Vamos tentar compilar? Digitei no terminal: javac TestaTipoDeReferenciaObjeto.java. Não encontrei o símbolo CNPJ dentro do tipo class Conta. Faz sentido! Repare que para o compilador não tem ainda os dados de execução, o compilador só consegue entender as referências, os tipos das variáveis.

O tipo da variável c é do tipo Conta. Conta tem ID? Tem. Conta tem CNPJ? Talvez sim, talvez não. Não dá para ter certeza, como não dá para ter certeza o compilador não compila. Nem toda conta tem CNPJ. Não compila. Essa é a diferença que nós temos que prestar atenção a todo momento em que estamos usando a herança.

Eu só posso invocar métodos e acessar variáveis membros que estejam definidas no meu tipo de referência e não no meu tipo que está sendo referenciado em tempo de execução, porque isso só o meu interpretador consegue descobrir. Isso daqui eu não vou poder fazer.

Vou compilar... E compila! Agora para deixar bem claro isso, olhe lá: (new ContaJuridica()], eu peguei uma ContaJuridica e coloquei entre parênteses para ficar bonito. Vou colocar um ID nela: (new ContaJuridica()].id = 15; Estou jogando fora, não estou fazendo mais nada com ela, mas posso fazer isso. Compilo porque toda ContaJuridica tem ID.

Vou pegar essa ContaJuridica e vou falar CNPJ dela igual a 15: (new ContaJuridica()].cnpj = 15;. Eu posso fazer com o CNPJ dela igual a 15 porque toda ContaJuridica tem CNPJ.

Polimorfismo é a capacidade de eu chamar o mesmo objeto de diversas formas. Eu posso referenciá-lo através de uma ContaJuridica, de uma conta ou até mesmo de object. Claro, quanto mais específico o escopo - isso é, ContaJuridica tem mais coisas que eu posso acessar e Conta tem menos coisas que eu posso acessar. O object menos ainda, porque só tem o que todos os objetos têm em comum. Tem que tomar esse cuidado!

Da mesma maneira, se eu quiser pegar agora essa ContaJuridica c e chamá-la de d, eu posso fazer isso agora? Isso é, pegar aquela minha Conta c ali e referenciar ela como d do tipo ContaJuridica. Para nós humanos faz todo sentido, porque eu sei que em tempo de execução vou ter uma ContaJuridica lá dentro, mas em tempo de compilação não.

Lembra que o compilador fala? O compilador só olha os tipos das variáveis, o tipo da variável c é compatível com ContaJuridica? Isso é, o tipo da variável c é Conta? Nem sempre, nem toda conta é jurídica; pode ser uma conta, como pode ser uma conta física. Não sabemos, pode ser qualquer outra conta que vão criar daqui há 50 anos, não sei. Por isso essa linha não compila.

Vamos testar? Tento compilar e não compila! Porque se é do tipo Conta nem todo Conta é do tipo ContaJuridica, não vai compilar. A mesma coisa vai valer para o método, eu dei exemplos aqui de chamada de utilização e acesso às variáveis membro, mas a mesma coisa vai funcionar para método.

Se eu tiver o método liga eu posso chamá-lo em uma conta, mas se só a classe jurídica tem o método fecha, eu preciso ter uma referência para uma ContaJuridica para poder chamar esse método.

Isso é, se eu tenho uma conta, eu posso chamar o método liga, mas eu não posso chamar o método fecha. Mesmo que em tempo de execução eu saiba que é da ContaJuridica, se a sua variável é do tipo Conta, a referência, o compilador não vai deixar. Vamos testar? O liga ele deve deixar e o fecha ele não deve deixar.

O liga ele deixou e o fecha ele não deixou! Mesmo em casos onde você pode falar: "Guilherme, todo mundo tem o método fecha. Eu vou fazer com que a classe Conta seja abstrata, vou colocar aqui um método abstrato fecha e, com isso, eu vou garantir que todo mundo tenha esse método fecha. Guilherme, todo mundo tem esse método fecha. Todo mundo tem que ter, senão não vai compilar".

Digitei void fecha() {}. Fiz o método fecha e todo mundo tem, agora eu posso chamar esse método fecha. Dessa maneira, eu posso chamar esse método fecha. Compilei. Por quê? Eu estou falando que a classe Conta tem o método fecha porque todo mundo que herda ele tem que implementar esse método fecha. Agora, se eu tirar daqui eu posso criar uma nova conta?

Por enquanto, eu só posso criar uma ContaJuridica e uma ContaFisica, toda conta, "teoricamente", poderia ter um método fecha, mas na prática você poderia criar uma classe interclass ou você poderia no futuro criar uma outra classe sem o método fecha. Nesse caso não pode, não compila porque a conta não tem o método fecha.

Só podemos invocar o método, acessarmos o membro, se ele está definido naquela classe que nós estamos referenciando em tempo de compilação. O binding para qual exatamente se é o do filho ou do pai que ele vai chamar é em tempo de execução que ele vai fazer, ele vai fazer lookup em tempo de execução. Porém, para garantir que existe o método é em tempo de compilação, você pegaria esse tipo de erro, do método não existir.

Em tempo de execução ele descobre se é para chamar o método do filho ou o método do pai. Você pode me falar: "Acalme-se aí, Guilherme! Nós fizemos tudo agora e no mesmo pacote".

Ainda temos dois casos mega interessantes para trabalharmos aqui. Vamos primeiro falar dos casos que trabalhamos até agora e que são mais simples, que do public. Se eu tenho um método na minha classe pai que é público e estou sobrescrevendo, pela regra de sobrescrita ele tem que ser público. Não tem graça, todo mundo tem acesso! Funciona.

Se o meu método na classe pai é protected todos os meus filhos e todo mundo que é do mesmo pacote tem acesso para compilar. Funciona. Claro, o meu filho pode definir o método como público porque ele está aumentando o escopo de acesso. Não tem muita graça!

Mas e se o meu método é package protected, aquele default padrão? Ou se ele é privado, o que acontece? Vamos testar? Vou criar duas classes: classe de conta e conta financeira. No pacote financeiro eu vou criar uma classe conta financeira, ela vai ser pública e ela vai estender de conta: class ContaFinanceira extends Conta { }, package financeiro; e import modelo.*;.

Dentro dessa nossa conta financeira eu vou ter o método fecha, que imprime financeira: void fecha() { System.out.println("financeira"); }. Essa aqui é a minha classe: ContaFinanceiraJava.

Vou criar também agora na classe no pacote modelo uma coisa muito parecida: package modelo;. Vou importar: import financeiro.*;. A classe se chama Conta e o método fecha imprime conta normal: System.out.println("conta normal");.

Vamos criar aqui um new file que se chama no pacote modelo TestaContaHeranca: public class TestaContaHeranca {}. Aqui dentro do meu método public static void main(String[] args) {} eu vou criar uma conta: Conta c = new ContaFinanceira;.

Na minha ContaFinanceira eu vou chamar o método fecha: c.fecha();. Vou chamar esse método de: TestaContaHeranca.Java, ele está no pacote aqui. Como eu tenho que importar também, o pacote financeiro vou deixar aqui.

Mesmo que o primeiro import seja inútil eu estou feliz e contente. Eu criei uma conta financeira, conta com método no fecha; eu tenho acesso ao método fecha. Vou compilar e rodar. Digitei javac modelo/TestaContaHeranca.java e compilei. Sobrescrevi então vou imprimir financeira.

Imprimiu normal, como assim!? Estamos trabalhando com os pacotes. Lembre-se que um tipo de padrão que temos aqui somente quem está no mesmo pacote tem acesso a esse método da classe Conta. Só que está no mesmo pacote sabe da existência do método fecha da classe Conta.

A classe ContaFinanceira que escreveu um método fecha nem sabe da existência do método fecha lá da classe pai, por isso ela não está sobrescrevendo nada. Porque o método fecha da classe pai é o método fecha da classe pai, que só o pai e quem está no mesmo pacote sabe da existência. Não estou sobrescrevendo, por mais que parece que eu esteja.

A mesma coisa aconteceria com o private. Se fosse private até mesmo no mesmo no pacote eu não estaria sobrescrevendo, porque eu nem saberia da existência do método na minha classe pai. Esse método aqui compila porque a classe Conta tem o método fecha, está no mesmo pacote e chama esse método, mas o método não foi sobrescrito. Compila e imprime a conta.

Se eu tentasse fazer aqui ContaFinanceira nem compilaria, porque o método fecha está em outro pacote, eu não tenho visibilidade a esse método fecha. Lembre-se que para compilar ele olha a referência, a referência de ContaFinanceira. Vamos tentar? Não compila! ContaFinanceira.fecha não é público, você não pode acessar em outro pacote, não é nem protected que você herda e etc., você não pode.

Muito cuidado aqui! Quando eu estou usando herança com métodos package protected ou protected, eu tenho que tomar o cuidado se eu estou realmente sobrescrevendo ou não esse método.

Só sobrescrevo se eu tenho visibilidade e acesso a esse método na minha classe pai, senão eu não estarei sobrescrevendo e criando um método novo que não tem nada haver com o método original e não vai chamar o método filho.

Como eu posso ver isso acontecer na prática? Quando eu crio aqui como Conta, eu posso falar que esse método está sobrescrevendo, @Override, é uma opção. Eu estou falando para o compilador que eu estou sobrescrevendo o método da classe pai. O compilador fala: "Não, Guilherme, você não está sobrescrevendo o método da classe pai" e aí eu percebo que estou fazendo alguma coisa errada.

O que estou fazendo errado? Esses dois métodos deveriam ser protected, se eles fossem protected, aí sim, a classe filha estaria sobrescrevendo o método do pai. Aí sim, eu poderia acessar esse método da minha classe filha. Vamos testar? Compilei, rodei e imprimi o ContaFinanceira.

Mesmo eu não tendo acesso direto à ContaFinanceira e método fecha, eu não tenho acesso direto porque esse método é protected naquele pacote, em tempo de compilação não teria acesso direto.

Mas indiretamente eu tenho, porque em tempo de compilação eu estarei acessando um método do mesmo pacote. Em tempo de execução ele vai fazer o lookup e vai encontrar o método para mim. Compilo e imprimo o financeiro.

Sobre o curso Certificação Java SE 7 Programmer I: relações entre classes

O curso Certificação Java SE 7 Programmer I: relações entre classes possui 116 minutos de vídeos, em um total de 61 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Plus

De
R$ 1.800
12X
R$109
à vista R$1.308
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

Matricule-se

Pro

De
R$ 2.400
12X
R$149
à vista R$1.788
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Luri, a inteligência artificial da Alura

    Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com Luri até 100 mensagens por semana.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas