0% acharam este documento útil (0 voto)
0 visualizações137 páginas

Java Básico

A apostila de Java, escrita por Alexandre Diefenthäler, aborda a história, características e fundamentos da linguagem Java, destacando sua evolução e aplicação em diferentes contextos. O documento explora conceitos como a máquina virtual Java, programação orientada a objetos, e as diferenças entre JRE, Java SE e JDK. Além disso, apresenta tópicos avançados, como exceções, coleções, e gerenciamento de threads, visando fornecer uma compreensão abrangente da linguagem.
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
0 visualizações137 páginas

Java Básico

A apostila de Java, escrita por Alexandre Diefenthäler, aborda a história, características e fundamentos da linguagem Java, destacando sua evolução e aplicação em diferentes contextos. O documento explora conceitos como a máquina virtual Java, programação orientada a objetos, e as diferenças entre JRE, Java SE e JDK. Além disso, apresenta tópicos avançados, como exceções, coleções, e gerenciamento de threads, visando fornecer uma compreensão abrangente da linguagem.
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd

Resumo Java

APOSTILA DE JAVA

GUAÍBA, RS – BRASIL
ENGENHEIRO E MESTRE
ALEXANDRE DIEFENTHÄLER
Índice de figuras
Índice de tabelas
Tabela 22.1: Composição vs. Agregação................................................................................126
Sumário
1. Introdução...............................................................................................................................1
2. Características......................................................................................................................... 2
2.1. Módulos, plataformas, etc...............................................................................................2
2.1.1. JRE e Java SE.......................................................................................................... 2
2.1.2. JRE e JDK............................................................................................................... 3
2.2. Máquina virtual...............................................................................................................3
2.2.1. Máquina virtual vs. Interpretador............................................................................5
2.2.2. Bytecode.................................................................................................................. 5
2.3. Hotspot e JIT................................................................................................................... 5
3. Linguagem.............................................................................................................................. 7
3.1. Variáveis primitivas......................................................................................................... 7
3.1.1. “int”......................................................................................................................... 7
3.1.2. “double”...................................................................................................................8
3.1.3. “boolean”................................................................................................................. 8
3.1.4. “char”.......................................................................................................................8
3.1.5. “Tamanho” dos tipos................................................................................................8
3.1.6. Tipos primitivos e valores........................................................................................9
3.1.7. Casting..................................................................................................................... 9
3.1.8. Pós-incremento e pré-incremento..........................................................................11
3.2. Bloco if-else.................................................................................................................. 12
3.3. Laços de repetição......................................................................................................... 12
3.3.1. Escopo das variáveis..............................................................................................12
4. Orientação a objetos básica...................................................................................................12
4.1. Uma classe em Java.......................................................................................................14
4.2. Criando e manipulando um objeto................................................................................14
4.3. Métodos......................................................................................................................... 15
4.3.1. Métodos com retornos........................................................................................... 16
4.3.2. Objetos são acessados por referências...................................................................17
4.4. Atributos........................................................................................................................ 20
4.5. Modificadores de acesso............................................................................................... 22
4.6. Encapsulamento.............................................................................................................24
4.7. Construtores...................................................................................................................25
4.7.1. Invocando outro construtor....................................................................................26
4.8. Atributos de classe.........................................................................................................26
4.9. Herança..........................................................................................................................28
4.10. Override.......................................................................................................................30
4.10.1. A anotação @Override.........................................................................................31
4.10.2. Invocando o método superior.............................................................................. 32
4.11. Polimorfismo............................................................................................................... 32
4.11.1. Herança versus Acoplamento...............................................................................34
5. Pacotes – suas classes e bibliotecas TESTE!!!..................................................................... 35
5.1. Diretórios.......................................................................................................................35
5.2. Import............................................................................................................................ 36
5.3. Pacote [Link]........................................................................................................... 38
5.3.1. Classe System........................................................................................................ 38
5.3.2. Pacote [Link]........................................................................................38
[Link]. Método toString.............................................................................................39
[Link]. Método equals................................................................................................40
[Link]. Casting de referências.................................................................................... 41
5.3.3. Pacote [Link]........................................................................................42
[Link]. Método “equals”............................................................................................ 42
[Link]. Método “compareTo”.....................................................................................43
[Link]. Concatenção...................................................................................................43
[Link]. Imutável......................................................................................................... 43
[Link]. Outros métodos..............................................................................................44
[Link]. Stringbuffer e Stringbuilder........................................................................... 45
6. JAVADOC............................................................................................................................. 45
7. Classes abstratas....................................................................................................................47
7.1. Métodos abstratos..........................................................................................................49
7.1.1. Declarar um método abstrato.................................................................................50
8. Interfaces............................................................................................................................... 51
9. Exceções e controle de erros................................................................................................. 56
9.1. Unchecked..................................................................................................................... 57
9.2. Checked......................................................................................................................... 59
9.3. Lançando exceções........................................................................................................60
9.4. Criando as próprias exceções........................................................................................62
9.5. Cláusula Finally.............................................................................................................63
10. Arrays.................................................................................................................................. 63
10.1. Arrays de referências................................................................................................... 63
10.2. Percorrendo um array..................................................................................................64
10.2.1. “enhanced-for”.....................................................................................................64
10.2.2. Dimensões........................................................................................................... 64
11. Collections...........................................................................................................................65
11.1. Percorrendo coleções...................................................................................................67
11.1.1. [Link].................................................................................................. 68
11.2. [Link]................................................................................................................ 70
11.2.1. ArrayList.............................................................................................................. 70
[Link]. Instanciar um ArrayList...............................................................................70
11.2.2. LinkedList............................................................................................................74
11.2.3. Vector................................................................................................................... 74
11.2.4. Método sort()....................................................................................................... 74
11.3. [Link].................................................................................................................77
11.3.1. HashSet................................................................................................................ 78
11.3.2. TreeSet................................................................................................................. 79
11.3.3. LinkedHashSet.....................................................................................................79
12. [Link]...................................................................................................................... 80
12.1. HashMap..................................................................................................................... 83
12.2. TreeMap.......................................................................................................................84
12.2.1. Árvore “red-balck”.............................................................................................. 84
12.2.2. Métodos de SortedMap e NavigableMap............................................................85
12.3. Hashtable..................................................................................................................... 86
12.3.1. Hashing................................................................................................................ 87
[Link]. Exemplo....................................................................................................... 89
12.4. Métodos equals e hashcode.........................................................................................90
12.5. Properties.....................................................................................................................91
12.6. Boas práticas................................................................................................................91
13. Interface Comparator.......................................................................................................... 92
14. Classes aninhadas................................................................................................................95
14.1.1. Não estática..........................................................................................................96
14.1.2. Estática.................................................................................................................96
14.1.3. Anônimas............................................................................................................. 96
15. Funções Lambda.................................................................................................................97
15.1. Threads........................................................................................................................ 98
15.2. Colletions.....................................................................................................................99
15.3. Listeners....................................................................................................................101
15.4. Lambda como argumentos........................................................................................103
16. Métodos default.................................................................................................................106
17. Pacote [Link]....................................................................................................................108
17.1. IOException...............................................................................................................108
17.2. InputStream...............................................................................................................108
17.2.1. FileInputStream.................................................................................................108
17.3. Reader........................................................................................................................109
[Link]. InputStreamReader.....................................................................................110
[Link].1. Lendo de um arquivo..........................................................................110
[Link].2. Lendo do teclado................................................................................111
[Link]. OutPutStream............................................................................................. 112
[Link]. Fechar arquivo............................................................................................113
18. Conversão..........................................................................................................................114
19. Classes wrapper.................................................................................................................115
20. Threads.............................................................................................................................. 116
20.1. Thread x Runnable.....................................................................................................117
20.2. Escalonador e trocas de contexto..............................................................................117
20.3. Concorrência..............................................................................................................119
20.3.1. Região crítica..................................................................................................... 119
[Link]. Sincronizando um bloco............................................................................120
[Link]. Sincronizando o método............................................................................120
21. Memória............................................................................................................................ 121
21.1. Heap x stack..............................................................................................................121
21.1.1. Heap...................................................................................................................121
21.1.2. Stack.................................................................................................................. 122
21.2. Garbage collector...................................................................................................... 123
21.2.1. [Link]()......................................................................................................123
21.2.2. Finalizer............................................................................................................. 124
22. Apêndice............................................................................................................................125
22.1. Composição............................................................................................................... 125
22.2. Acoplamento..............................................................................................................126
22.3. Questões....................................................................................................................127
1

1. Introdução

Entender um pouco da história da plataforma Java é essencial para enxergar os motivos que a
levaram ao sucesso.

Quais eram os seus maiores problemas quando se programava na década de 1990? Ponteiros?
Gerenciamento de memória? Organização? Falta de bibliotecas? Ter de reescrever parte do código ao
mudar de sistema operacional? Custo financeiro de usar a tecnologia?
A linguagem Java resolve bem esses problemas que, até então, apareciam com frequência nas
outras linguagens. Alguns desses problemas foram, particularmente, atacados porque uma das grandes
motivações para a criação da plataforma Java era de que essa linguagem fosse usada em pequenos
dispositivos, como TVs, videocassetes, aspiradores, liquidificadores e outros. Apesar disso, a linguagem
teve seu lançamento focado no uso em clientes web (browsers) para rodar pequenas aplicações
(applets). Hoje em dia, esse não é o grande mercado do Java, embora tenha sido idealizado com um
propósito e lançado com outro, o Java ganhou destaque no lado do servidor.
A Sun criou um time (conhecido como “Green Team”) para desenvolver inovações
tecnológicas em 1992. Essa equipe foi liderada por James Gosling, considerado o pai do Java. O grupo
teve a ideia de criar um interpretador (já era uma máquina virtual) para pequenos dispositivos,
facilitando a reescrita de software para aparelhos eletrônicos, como videocassete, televisão e aparelhos
de TV a cabo.
A ideia não deu certo. Tentaram fechar diversos contratos com grandes fabricantes de
eletrônicos, como a Panasonic, mas não houve êxito devido ao conflito de interesses e custos. Hoje,
sabemos que o Java domina o mercado de aplicações para celulares com mais de 2.5 bilhões de
dispositivos compatíveis. Porém, em 1994, ainda era muito cedo para isso.
A semelhança era que, na internet, havia uma grande quantidade de sistemas operacionais e
browsers e, com isso, seria uma grande vantagem poder programar em uma única linguagem,
independente da plataforma. Foi aí que o Java 1.0 foi lançado: focado em transformar o browser de
apenas um cliente magro (“thin client” ou terminal burro) em uma aplicação que possa também realizar
operações avançadas, e não apenas renderizar HTML.
Os applets deixaram de ser o foco da Sun, e a Oracle nunca teve interesse nisso. É curioso
notar que a tecnologia Java nasceu com um objetivo em mente e foi lançada com outro, mas, no final,
decolou mesmo no desenvolvimento de aplicações do lado do servidor.
2

2. Características
Além do paradigma da programação orientada a objetos, java possui outras características
que diferem de outras linguagens de programação, como C ou Pascal que utilizam a programação
procedural.

No decorrer do estudo desta apostila, você pode achar que o Java tem menor produtividade
quando comparado com a linguagem que está acostumado, mas é preciso ficar claro que a premissa
do Java não é a de criar mais rapidamente sistemas pequenos nos quais temos um ou dois
desenvolvedores de linguagens do tipo PHP, Perl e outras.

O foco da plataforma é outro: aplicações de médio a grande porte, em que o time de


desenvolvedores tem várias pessoas e sempre pode vir a mudar e crescer. Não tenha dúvidas que
criar a primeira versão de uma aplicação usando Java, mesmo utilizando IDEs e ferramentas
poderosas, será mais trabalhoso que muitas linguagens script ou de alta produtividade. Porém, com
uma linguagem orientada a objetos e madura como o Java, será extremamente mais fácil e rápido
fazer alterações no sistema desde que você siga as boas práticas e recomendações sobre design
orientado a objetos.

2.1. Módulos, plataformas, etc.

Para executar e desenvolver programas em Java é necessário baixar os módulos,


plataformas, etc. Em um micro. Estes “itens” são chamados de JRE, Java SE e JDK. Suas
características e diferenças são apresentadas nos quadros abaixo.

2.1.1. JRE e Java SE


3

2.1.2. JRE e JDK

2.2. Máquina virtual

Em uma linguagem de programação como C e Pascal, temos a seguinte situação quando


vamos compilar um programa:
Figura 1: compilação "clássica".

Conforme apresentou a figura 1, o código fonte é compilado para um código de máquina


específico de uma plataforma e sistema operacional. Muitas vezes, o próprio código fonte é
desenvolvido visando uma única plataforma.

Esse código executável (binário) resultante será manipulado pelo sistema operacional e,
por esse motivo, ele deve saber conversar com o sistema operacional em questão.
4

Figura 2: executável para cada S.O.

Conforme apresentou a figura 2, temos um código executável para cada sistema


operacional. Portanto, é necessário compilar uma vez para Windows, outra para o Linux e assim por
diante caso seja necessário que esse software seja utilizado em várias plataformas. Esse é o cenário
de aplicativos como o OpenOffice, Firefox e outros.

Como foi apresentado, na maioria das vezes, a uma aplicação se vale das bibliotecas do
sistema operacional, por exemplo, a de interface gráfica para desenhar as telas, sendo a biblioteca
de interface do Windows bem diferente das do Linux: como criar, então, uma aplicação que rode de
forma parecida nos dois sistemas operacionais?

A primeira solução já foi indicada: precisamos reescrever um mesmo pedaço da aplicação


para diferentes sistemas operacionais, já que eles não são compatíveis.

Para a segunda solução, o Java utiliza o conceito de “máquina virtual” no qual existe, entre
o sistema operacional e a aplicação, uma camada extra que é responsável, principalmente e entre
outras coisas, por traduzir as respectivas chamadas de um sistema operacional que uma aplicação
invoca para o programa. Considere o esquemático apresentado na figura 3.

Figura 3: esquema com uma Virtual Machine.

Conforme apresentou a figura 3, a maneira em que se abre uma janela no Linux ou no


Windows é a mesma, obtendo-se independência de sistemas operacionais. Ou, melhor ainda,
independência de plataforma em geral: não é preciso se preocupar em qual sistema operacional sua
aplicação está rodando nem em que tipo de máquina, configurações etc.
5

2.2.1. Máquina virtual vs. Interpretador

É preciso ficar claro que uma máquina virtual é um conceito bem mais amplo que o de um
interpretador. Como o próprio nome diz, uma máquina virtual é como um computador virtual sendo
responsável por gerenciar memória, threads, a pilha de execução etc.

Uma aplicação roda sem nenhum envolvimento com o sistema operacional nativo,
interagindo apenas com a Java Virtual Machine (JVM).

Essa característica é interessante: como tudo passa pela JVM, ela pode tirar métricas,
decidir em qual lugar é melhor alocar a memória, além de isolar totalmente a aplicação do sistema
operacional. Se uma Java Virtual Machine termina abruptamente, só as aplicações que estavam
rodando nela terminarão: isso não afetará outras JVMs que estejam rodando no mesmo computador
nem o sistema operacional.

Essa camada de isolamento também é interessante quando pensamos em um servidor que


não pode se sujeitar a rodar código que possa interferir na boa execução de outras aplicações.

“WRITE ONCE, RUN ANYWHERE”. Esse era um slogan que a Sun usava para o Java, já
que não é necessário reescrever partes de uma aplicação toda vez que quiser mudar de sistema
operacional.

2.2.2. Bytecode

É importante ficar claro que a máquina virtual não processa código Java, mas um código
de máquina específico que é gerado por um compilador Java (javac) e é conhecido por “bytecode”,
pois existem menos de 256 códigos de operação dessa linguagem, pois cada “opcode” gasta um
byte. O compilador Java gera esse bytecode que, diferente das linguagens sem máquina virtual, será
“interpretado” pela JVM para diferentes sistemas operacionais.

2.3. Hotspot e JIT

Hotspot é a tecnologia que a JVM utiliza para detectar “pontos quentes” de uma aplicação:
código que é muito executado, provavelmente, dentro de um ou mais “loops”. Quando a JVM julgar
necessário, ela vai compilar esses códigos para instruções realmente nativas da plataforma, tendo
em vista que isso irá provavelmente melhorar a performance da sua aplicação. Esse compilador é o
JIT: “Just inTime Compiler”, que aparece bem na hora em que precisa.

Então, por que a JVM não compila tudo antes de executar a aplicação? É que,
6

teoricamente, compilar de forma dinâmica, na medida do necessário, pode gerar uma performance
melhor. O motivo é simples: imagine um “.exe” gerado pelo VisualBasic, gcc ou Delphi. Ele é
estático e já foi otimizado com base em heurísticas 1. Ou seja, o compilador pode ter tomado uma
decisão não tão boa.

Já a JVM, por compilar dinamicamente durante a execução, pode perceber que um


determinado código não está com a performance adequada e otimizar mais um pouco aquele trecho
ou ainda mudar a estratégia de otimização. É por esse motivo que as JVMs mais recentes, em
alguns casos, chegam a ganhar de códigos C compilados com o GCC 3.x.

1 Heurística é um procedimento mental simples que ajuda a encontrar respostas adequadas, embora várias vezes
imperfeitas, para perguntas difíceis.[1] A palavra tem a mesma raiz que eureca. Fonte:
[Link]
7

3. Linguagem
Nos próximos subcapítulos, estudaremos a linguagem Java, como a estrutura de um
programa, as suas variáveis, os atributos, os métodos, etc.

3.1. Variáveis primitivas

A declaração de variáveis em java não é diferente de outras linguagens, com exceção das
que tem “tipagem fraca” (javascript, Python etc.), pois java utiliza “tipagem forte”, o que significa
que, uma vez definido o tipo de variável, este não pode mais ser mudado.

3.1.1. “int”

O tipo inteiro é declarado pela palavra-chave “int”, e segue o mesmo como em todas as
outras linguagens de programação. Exemplo: int idade.

Após declarar uma variável, a partir da declaração, podemos atribuir um valor a ela;
Exemplo: idade = 15. Por fim, podemos imprimir o valor desta variável conforme apresenta a figura
4.

Figura 4: exemplo de variável "int".

No mesmo momento em que se declara uma variável, também é possível inicializá-la por
praticidade. Exemplo: int idade = 15. Assim, um exemplo completo da utilização da variável
anterior é apresentado na figura 5.

Figura 5: outro exemplo de variável "int".


8

3.1.2. “double”

Representar números inteiros é fácil, mas como guardar valores reais, tais como frações de
números inteiros e outros? Outro tipo de variável muito utilizado é o double que armazena um
número com ponto flutuante (e que também pode armazenar um número inteiro). Exemplos:

Figura 6: exemplo de variável "double".

3.1.3. “boolean”

O tipo boolean armazena um valor verdadeiro ou falso: nada de números, palavras ou


endereços como em algumas outras linguagens. Exemplo: boolean verdade = true.

As palavras “true” e “false” são reservadas ao Java. É comum que um boolean seja
determinado por meio de uma expressão booleana. Isto é, um trecho de código que retorna um
booleano. Exemplo:

Figura 7: exemplo de variável "booleana".

3.1.4. “char”

O tipo “char” guarda um, e apenas um, caractere. Este deve estar entre aspas simples. Não
se esqueça dessas duas características de uma variável do tipo “char”. Por exemplo, ela não pode
guardar um código como ' ', pois o vazio não é um caractere.

3.1.5. “Tamanho” dos tipos

No quadro abaixo, estão os tamanhos de cada tipo primitivo do Java.


9

3.1.6. Tipos primitivos e valores

Esses tipos de variáveis são os “tipos primitivos” do Java: o valor que elas guardam são o
real conteúdo da variável. Quando se utilizar o operador de atribuição ‘=’, o valor será copiado.
Exemplos:

Figura 8: exemplo de atribuição de "primitivos'.

No exemplo da figura 8, o ‘i’ fica com o valor de 6, mas e ‘j’? Na segunda linha, ‘j’ está
valendo 5. Quando ‘i’ passa a valer 6, ‘j’ também muda de valor? Não, pois o valor de um tipo
primitivo sempre é copiado. Ou seja, apesar de a linha 2 fazer “j = i”, a partir desse momento, essas
variáveis não têm relação nenhuma: o que acontece com uma não reflete em nada na outra.

3.1.7. Casting

É importante saber que o casting ocorre de duas formas: do tipo maior para o menor e deve
ser indicado explicitamente, e do menor para o maior que, neste caso, é chamado de “promoção” e
ocorre, na maioria das vezes, de forma implícita. Por exemplo: double d = 5. O compilador Java
infere que 5 é um inteiro, mas o “promove” ao tipo double, implicitamente.

Alguns valores são incompatíveis ao tentar efetuar uma atribuição direta. Enquanto um
número real em java, por padrão, é representado em uma variável do tipo “double”, tentar atribui-lo
a uma variável int não funciona. Considere o exemplo apresentado na figura 9.

Figura 9: atribuição incompatível.

A mesma incompatibilidade ocorre no seguinte trecho: int i = 3.14. Nem mesmo o seguinte
código compila:

Figura 10: outra atribuição incompatível.

Apesar de 5 ser um bom valor para um int, o compilador não tem como saber qual valor
estará dentro daquele tipo double no momento da execução. Esse valor poderá ser sido digitado pelo
usuário, e ninguém garantirá que essa conversão ocorra sem perda de valores.
10

Mas, às vezes, precisamos que um número quebrado (double ou float) seja arredondado e
armazenado em um número inteiro. Para fazer isso sem que haja o erro de compilação, é preciso
indicar que o número quebrado seja moldado como um número inteiro. Esse processo recebe o
nome de “casting”, conforme apresenta a figura 11.

Figura 11: exemplo de "casting".

O casting foi eleborado para “moldar” a variável “d3” como um int. O valor de i agora é 3.
O mesmo caso ocorre entre valores int e long, conforme apresenta a figura 12.

Figura 12: exemplo sem "casting".

Toda via, é possível efetuar o “casting”, conforme apresenta a figura 13.

Figura 13: solução com "casting".

Outro tipo de casting é necessário devido a seguinte atribuição: float x = 0.0. Esse código
não compila, pois todos os literais com ponto flutuante são considerados double pelo Java, e o tipo
float não pode receber um double sem a perda de informação. Para fazê-lo funcionar, podemos
escrever: float x = 0.0f. A letra ‘f’, que pode ser maiúscula ou minúscula, indica que aquele literal
deve ser tratado como float. Considere outro caso, conforme apresenta a figura 14.

Figura 14: cálculo sempre no maior tipo.

Nesses casos, sempre se deve fazer uso do casting, porque o Java efetua os cálculos e
efetua o armazenando sempre no maior tipo que apareceu durante as operações, neste caso, o
double. No entanto, considere o exemplo da figura 15.
11

Figura 15: mais um caso de "casting" por "promoção".

O código acima compila sem problemas, uma vez que um double pode guardar um número
com ou sem ponto flutuante. Todos os inteiros representados por uma variável do tipo int podem ser
guardados em uma variável double, então, não existem problemas no código da figura 15. Este tipo
de casting é chamado de “promoção”.

No quadro da figura abaixo, estão relacionados todos os castings possíveis na linguagem


Java, mostrando a conversão de um valor em outro. A indicação Impl. quer dizer que aquele cast é
implícito e automático e é o de “promoção”, ou seja, não é necessário indicar o casting
explicitamente (lembrando que o tipo boolean não pode ser convertido em nenhum outro tipo).

Não é necessário saber o tamanho exato desses tipos para o casting (de promoção ou não),
mas deve-se saber qual é maior. Por exemplo, deve-se saber que o long ocupa muito mais espaço do
que um int, que, por sua vez, ocupa mais espaço do que um short e assim por diante.

3.1.8. Pós-incremento e pré-incremento

Considere que o código “i = i + 1” pode, realmente, ser substituído por i++ quando isolado.
Porém, em alguns casos, temos essa instrução envolvida em, por exemplo, uma atribuição.
Exemplo:

Qual é o valor de ‘x’? O de i, após essa linha, é 6, e a resposta depende de como funciona o
operador ++ que, quando é inserido após a variável, retorna o valor antigo e o incrementa (pós-
incremento) fazendo x valer 5.
12

Se tivéssemos inserido o operador ++ antes da variável (pré-incremento), o resultado seria


x valer 6. Exemplo:

3.2. Bloco if-else

Igual às demais linguagens de programação.

3.3. Laços de repetição

Tanto o “for” quanto o “while” e o “do while” são iguais às demais linguagens de
programação.

3.3.1. Escopo das variáveis

No Java, podemos declarar variáveis a qualquer momento. Porém, dependendo de onde


você as declarou, ela valerá de um determinado ponto a outro. Exemplo:

Portanto, o escopo da variável é o nome dado ao trecho de código em que aquela variável
existe e o lugar onde é possível acessá-la.
Quando abrimos um novo bloco com as chaves, as variáveis declaradas ali dentro só valem
até o fim daquele bloco. Exemplo:

4. Orientação a objetos básica


Orientação a objetos é uma maneira de programar que ajuda na organização e resolve
13

muitos problemas enfrentados pela programação procedural.

Consideremos o clássico problema da validação de um CPF: normalmente, temos um


formulário no qual recebemos essa informação e, depois, temos de enviar esses caracteres a uma
função que vai validá-lo, como no pseudocódigo abaixo:

cpf = formulario→campo_cpf;

valida(cpf);

Alguém o obriga a sempre validar esse CPF? Você pode, inúmeras vezes, esquecer de
chamar esse validador, pois considere que você pode ter 50 formulários e precise validar em todos o
CPF. Se sua equipe tem três programadores trabalhando nesses formulários, quem fica responsável
por essa validação? Resposta: todos!

A situação pode piorar: na entrada de um novo desenvolvedor, precisaríamos avisá-lo de


que sempre devemos validar o CPF de um formulário. É nesse momento que nascem aqueles guias
de programação para o desenvolvedor que for entrar nesse projeto – às vezes, é um documento
enorme. Em outras palavras, todo desenvolvedor precisa saber de uma quantidade enorme de
informações que, na maioria das vezes, não está realmente relacionada à sua parte no sistema, mas
ele precisa ler tudo isso, resultando em um entrave muito grande.

Outra situação na qual ficam claros os problemas da programação procedural é quando nos
encontramos na necessidade de ler o código que foi escrito por outro desenvolvedor e descobrir
como ele funciona internamente. Um sistema bem encapsulado não deveria gerar essa necessidade.
Em um sistema grande, simplesmente não temos tempo de ler todo o código existente.

Considerando que você não erre nesse ponto e a sua equipe tenha uma comunicação muito
boa (perceba que comunicação excessiva pode ser prejudicial e atrapalhar o andamento), ainda
temos outro problema: imagine que, em todo formulário, você também queira que a idade do cliente
seja validada – o cliente precisa ter mais de 18 anos. Teríamos de colocar um “if”, mas onde?
Espalhado por todo seu código e, mesmo que se crie outra função para validar, precisaríamos incluir
isso nos nossos 50 formulários já existentes. Qual é a chance de esquecermos um deles? Resposta: é
muito grande.

A responsabilidade de verificar se o cliente tem ou não 18 anos ficou espalhada por todo o
seu código. Seria interessante poder concentrar essa responsabilidade em um lugar só para não ter
chances de se esquecer disso.

Melhor ainda seria se conseguíssemos mudar essa validação e os outros programadores


14

nem precisassem ficar sabendo disso. Em outras palavras, eles criariam formulários, e um único
programador seria responsável pela validação: os outros nem saberiam da existência desse trecho de
código. Impossível? Não, o paradigma da orientação a objetos facilita tudo isso.

O problema do paradigma procedural é que não existe uma forma simples de criar conexão
forte entre dados e funcionalidades. No paradigma orientado a objetos, é muito fácil ter essa
conexão por meio dos recursos da própria linguagem.

4.1. Uma classe em Java

Para entendermos melhor o conceito de classe, vamos utilizar como exemplo a conta-
corrente de uma pessoa em um banco. Começaremos apenas com o que uma conta tem, e não com o
que ela faz (veremos isso logo em seguida). Podemos dizer que uma conta-corrente tem um
número, um cliente e um saldo. Um tipo desses, como o especificado, pode ser facilmente traduzido
para Java:

Por enquanto, declaramos o que toda conta deve ter. Esses são os atributos que as contas
quando criadas terão. Repare que essas variáveis foram declaradas fora de um bloco, diferente do
que fazíamos quando tinha aquele “main”. Quando uma variável é declarada diretamente dentro do
escopo da classe, é chamada de variável de objeto ou atributo.

4.2. Criando e manipulando um objeto

Já temos uma classe em Java que especifica o que todo objeto dessa classe deve ter. Mas
como usá-la? Além dessa classe, ainda teremos o “[Link]” e, a partir dele, utilizaremos a
classe Conta.

Para criar (“construir” ou instanciar) um objeto da classe Conta, basta usar a palavra-chave
“new”. Exemplo:
15

Pode parecer estranho escrevermos duas vezes Conta: uma vez na declaração da variável e,
outra vez, no uso do “new()”, mas há um motivo que, em breve, entenderemos. A partir de agora,
por meio da variável “minhaConta”, podemos acessar o objeto recém-criado para alterar seu titular,
seu saldo, etc. Exemplo:

É importante fixar que o ponto foi utilizado para acessar algo na variável “minhaConta”
que pertence ao titular “Duke” e tem saldo de mil reais.

4.3. Métodos

Dentro da classe, também declararemos o que cada conta faz e como isso é feito – os
comportamentos que cada classe tem. Por exemplo, de que maneira é possível sacar o dinheiro de
uma Conta? Especificaremos isso dentro da própria classe Conta, e não em um local desatrelado das
informações da própria Conta. É por isso que essas funções são chamadas de métodos, pois é a
maneira de fazer uma operação com um objeto.

Queremos implmenentar um método que permite sacar uma determinada quantidade e não
precisa devolver nenhuma informação para quem acionar esse método. Exemplo:

A palavra-chave “void” informa que nenhuma informação será enviada de volta a quem
invocou aquele método.
16

Quando alguém pedir para sacar, ela também dirá o quanto quer sacar. Por isso, precisamos
declarar o método com algo dentro dos parênteses – o que vai aí dentro é chamado de “parâmetro” e
o valor atribuído é chamado de “argumento”. Portanto, o parâmetro é uma variável comum,
chamada também de temporária ou local, pois, ao final da execução de um método, ela deixa de
existir.

Dentro do método, estamos declarando uma nova variável que, assim como o parâmetro,
desaparecerá no fim da execução das instruções dentro do bloco do método, porque esse é seu
escopo. No momento em que vamos acessar o atributo, usamos a palavra-chave “this” para indicar
que esse é um atributo que pertence a um objeto instanciado a partir da classe, e não uma simples
variável. Da mesma forma e na mesma classe, podemos adicionar um método para depositar alguma
quantia. Exemplo:

Para mandar uma mensagem ao objeto e pedir que ele execute um método, também
usamos o ponto. O termo usado para isso é invocação de método. É importante salientar
justamente isto: os métodos são procedimentos que são “acionados” por mensagens. Exemplos:

4.3.1. Métodos com retornos

Um método sempre tem de estabelecer o que retorna, mesmo definindo que não há retorno,
como nos exemplos anteriores, nos quais estávamos usando o “void”.
17

Um método pode retornar um valor para o código que o chamou. No caso do nosso método
“saca”, podemos alterá-lo para devolver um valor “booleano” indicando que a operação foi bem-
sucedida. Exemplo:

4.3.2. Objetos são acessados por referências

Quando declaramos uma variável para associar a um objeto, na verdade, essa variável não
guarda o “objeto”, mas, sim, uma maneira de acessá-lo, chamada de referência que aponta para uma
posição da memória onde, dentro do paradigma da orientação a objetos, é organizada de uma
maneira específica. É por esse motivo que, diferente dos tipos primitivos como int e long,
precisamos invocar o “new” depois de declarada a variável. Exemplo:

O correto aqui é dizer que “c1 se refere” a um objeto, e não que “c1 é um objeto”, pois é
uma variável referência. Basta lembrar que, em Java, uma variável nunca é um objeto. Não há, no
Java, uma maneira de criarmos o que é conhecido como objeto pilha ou objeto local, pois todo
objeto, nessa linguagem, sem exceção, é acessado por uma variável referência. Assim, este código
nos deixa na seguinte situação, conforme apresenta a figura abaixo:
18

Internamente, as referências c1 e c2 vão guardar um número que identifica em que posição


da memória objetos da classe Conta se encontram. Desta maneira, ao utilizarmos o ‘.’ para navegar,
o Java acessará um objeto da classe Conta que se encontra naquela posição de memória, e não
outra.

Para quem reconhece, é parecido com um ponteiro, porém, você não pode manipulá-lo
como um número nem utilizá-lo para aritmética, pois ela é tipada. Para reforçar ainda mas a
diferença entre uma variável e uma referência, vamos observar o exemplo apresentado na figura
abaixo:

O que acontece aqui? O operador = copia o valor de uma variável, mas qual é o valor da
variável c1? É o objeto? Não. Na verdade, o valor guardado é a referência (endereço) ao local onde
o objeto se encontra na memória principal. Na memória, o que acontece nesse caso é apresentado na
figura abaixo:
19

Quando ocorre c2 = c1, c2 passa, nesse instante, a fazer referência ao mesmo objeto
referenciado por c1. Então, nesse código em específico, quando utilizamos c1 ou c2, estamos nos
referindo exatamente ao mesmo objeto. Elas são duas “variáveis” distintas, porém, referenciam (ou
apontam) para o mesmo objeto. Compará-las com "==" retornará “true”, pois o valor que elas
carregam é o mesmo.

Isto ocorre porque o operador “==”, para valores primitivos, compara o conteúdo das
variáveis, mas essas variáveis não guardam valores primitivos, mas, sim, referências a objetos que
são dados complexos ou compostos, portanto, referencia ou apontam (não são ponteiros!) o
endereço em que o dado complexo se encontra.

E se quisermos ter um método que transfere dinheiro entre duas contas? Podemos ficar
tentados a criar um método o qual recebe dois parâmetros: conta1 e conta2 do tipo Conta, mas
cuidado: assim estamos pensando de maneira procedural.

A ideia é que, quando invocarmos o método “transfere”, já teremos um objeto do tipo


Conta (o “this”). Portanto, o método recebe apenas um parâmetro do tipo Conta, isto é, a Conta
destino e o valor. Exemplo:

Para deixar o código mais robusto, poderíamos verificar se a conta tem a quantidade a ser
transferida disponível. Com o intuito de ficar ainda mais interessante, você pode chamar os métodos
deposita e saca já existentes para fazer essa tarefa. Exemplo:
20

Quando passamos um objeto da classe Conta como argumento, o que será que acontece na
memória? Será que o objeto é clonado?

No Java, a passagem por parâmetros funciona como uma simples atribuição tal qual no uso
do ‘=’. Então, esse parâmetro copiará o valor da variável do tipo Conta que for passado como
argumento. E qual é o valor de uma variável dessas? Seu valor é um endereço ou uma referência,
mas nunca um objeto. Por isso, não há cópia de objetos.

4.4. Atributos

As variáveis do tipo atributo, diferentemente das variáveis temporárias (declaradas dentro


de um método), recebem um valor padrão. Por exemplo: no caso numérico, valem 0 e, no caso de
boolean, valem “false”. Você também pode dar valores default, como apresenta a figura abaixo:

Imagine que precisamos melhorar a classe Conta adicionando nome, sobrenome e CPF do
titular da conta. Começaríamos a ter muitos atributos e, se pensamos direito, uma Conta não tem
nome nem sobrenome nem CPF. Quem tem esses atributos é um Cliente. Deste modo, podemos
criar uma classe e fazer uma Composição onde os atributos da classe Conta também podem ser
referências a outras classes. Suponha a seguinte classe Cliente:
21

Um sistema orientado a objetos é um grande conjunto de classes que se comunicará


delegando responsabilidades a quem for mais apto a realizar determinada tarefa.

Por exemplo: uma classe Banco usa a classe Conta que usa a classe Cliente que, por sua
vez, usa a classe Endereço, etc. Dizemos que esses objetos colaboram, trocando mensagens entre si.
Por isso, temos muitas classes em nosso sistema, e elas costumam ter um tamanho relativamente
curto. Porém, e se dentro do código eu não invocasse “new” em Cliente e tentasse acessá-lo
diretamente? Exemplo:

Quando invocamos “new” em um objeto, ele o inicializa com seus valores default: 0 para
números, false para boolean e “null” para referências. “null” é uma palavra-chave em Java que
indica que não há nenhuma referência na memória para um objeto. Neste exemplo, a representação
na memória seria como apresenta a figura abaixo:

Se, em algum caso, você tentar acessar um atributo ou método de um objeto que está
referenciado como “null”, receberá um erro durante a execução (NullPointerException, que
veremos mais à frente). Percebe-se, então, que o new não apresenta um efeito cascata, a menos que
você dê um valor default, conforme apresenta a figura abaixo:
22

Com este código, todo novo objeto instanciado da classe Conta já terá um novo objeto da
classe Cliente associado, portanto, sem a necessidade de instanciá-lo logo em seguida da
instanciação de uma classe Conta. Qual alternativa você deve usar? Depende do caso: para toda
nova Conta, você precisa de um novo Cliente? É essa pergunta a ser respondida. Nesse caso, a
resposta é não, mas depende do nosso problema.

4.5. Modificadores de acesso

Um dos problemas com o exemplo usado até o momento é o acesso direto ao atributo
saldo. Isto não apenas permite que qualquer valor seja atribuído àquele atributo (valores negativos,
por exemplo), como, também, exige que o mesmo seja sempre testado toda vez que sofrer uma
alteração.

A melhor forma de resolver esta questão seria forçar quem instancia um objeto da classe
“Conta”, invocar o método “saca” e não permitir o acesso direto ao atributo. É o mesmo caso da
validação de CPF. Para implementar essa restrição de acesso no Java, basta declarar que os atributos
não podem ser acessados diretamente por um objeto da classe, por meio da palavra-chave “private”.
Exemplo:

A palavra-chave “private” é um modificador de acesso, também chamado de modificador


de visibilidade. Assim, marcando um atributo como privado (private), impede-se o acesso direto
em relação a todas as outras classes e fazemos com que o seguinte código não compile:

Na orientação a objetos, esta é uma prática quase que obrigatória: a de proteger seus
atributos com o modificador de acesso (ou visibilidade) “private”.
23

Cada classe é responsável por controlar seus atributos, portanto, ela deve julgar se aquele
novo valor é válido ou não. Essa validação não deve ser controlada por quem está usando a classe,
mas, sim, por ela mesma, centralizando essa responsabilidade e facilitando futuras mudanças no
sistema. Muitas outras vezes, nem mesmo queremos que outras classes saibam da existência de
determinado atributo, escondendo-o por completo, já que ele diz respeito ao funcionamento interno
do objeto.

Repare: quem invoca o método saca não faz a menor ideia de que existe uma verificação
para o valor do saque. Para quem for usar essa classe, basta saber o que o método faz e não como
exatamente ele o faz (o que um método faz é sempre mais importante do que como ele faz: mudar a
implementação é fácil, já mudar a assinatura de um método gerará problemas).

Portanto, a palavra-chave “private” também pode ser usada a fim de modificar o acesso a
um método. Tal funcionalidade é utilizada em diversos cenários, os mais comuns são quando existe
um método que serve apenas para auxiliar a própria classe, e quando há código repetido dentro de
dois métodos da classe. Sempre devemos expôr o mínimo possível de funcionalidades com o intuito
de criar um baixo Acoplamento entre as nossas classes.

Da mesma maneira que temos o “private”, temos o modificador de acesso “public” que
permite a todos acessarem um determinado atributo ou método. Exemplo:
Figura 16: exemplo de acoplamento.

É muito comum e faz todo sentido que seus atributos sejam “private”, e quase todos seus
métodos sejam “public” (mas não é uma regra). Dessa forma, toda interação de um objeto com
outro é feita por troca de mensagens, isto é, acessando seus métodos.

Se, eventualmente, precisarmos mudar como é realizado um saque na classe Conta, qual o
único local onde precisaríamos modificar? Apenas no método “saca”, o que faz sentido. Por
exemplo, imagine cobrar CPMF de cada saque: basta você modificar no método saca, e nenhum
outro código fora a classe Conta precisará ser recompilado. Além disso, as classes as quais usam
esse método nem precisam ficar sabendo de tal modificação. Você precisa apenas recompilar aquela
classe e substituir aquele arquivo “.class”. Ganhamos muito em esconder o funcionamento do nosso
método na hora de fazer manutenção e modificações.
24

IMPORTANTE: quando criamos uma classe com todos os atributos privados, seus getters,
setters e um construtor vazio (padrão), na verdade, estamos criando um “Java Bean”.

Assim, os modificadores de acesso existentes em Java são quatro:

1. “public” – todos podem acessar aquilo que for definido como public, inclusive classes de
outros pacotes. Classes, atributos, construtores e métodos podem ser public;

2. “Package” (padrão, sem nenhum modificador declarado) - se nenhum modificador for


utilizado, todas as classes do mesmo pacote têm acesso ao atributo, ao construtor, ao
método ou à classe;

3. “protected” – aquilo que é protected pode ser acessado diretamente por todas as classes do
mesmo pacote e por todas as classes que o estendam, mesmo que estas não estejam no
mesmo pacote. Somente atributos, construtores e métodos podem ser protected; e

4. “private” – a única classe capaz de acessar os atributos, construtores e métodos privados


diretamente é a própria classe. Classes, como conhecemos, não podem ser private, mas
atributos, construtores e métodos, sim.

IMPORTANTE: a palavra-chave “static” não é um modificar de acesso, pois indica que um


membro pertence à própria classe, e não a um objeto específico (instância) dela. Isso significa que o
elemento é compartilhado globalmente entre todas as instâncias da classe e pode ser acessado
diretamente pelo nome da classe, sem a necessidade de criar um objeto usando o operador

4.6. Encapsulamento

O que começamos a ver nesse capítulo é a ideia de encapsular, isto é, ocultar todos os
membros de uma classe (como vimos no subcapítulo anterior), além de ocultar as implementações
das rotinas (no caso, métodos) do sistema.

Encapsular é fundamental para um sistema ser suscetível a mudanças, pois, assim, não
precisaremos mudar uma regra de negócio em vários lugares, mas apenas um único lugar, já que
essa regra está encapsulada. Veja o caso do método “saca” conceitualmente, na imagem abaixo:
25

O conjunto de métodos públicos de uma classe é também chamado de interface da classe,


pois essa é a única maneira pela qual você se comunica com objetos dessa classe.

Ou seja, em termos práticos, você restringe o acesso direto aos atributos de um objeto e
disponibiliza métodos públicos específicos para manipular esses dados. A implementação baseia-se
em duas etapas principais, conforme enumerado abaixo:

1. Tornar os atributos privados: utiliza-se o modificador de acesso “private” para impedir que
outras classes modifiquem as variáveis diretamente; e

2. Criar métodos Getters e Setters: métodos públicos (public) que servem como intermediários
protegidos para obter (get) ou alterar (set) os valores.

4.7. Construtores

Quando usamos a palavra-chave “new”, estamos construindo um objeto, pois ele executa o
construtor da classe que é um bloco declarado com o mesmo nome que a classe. Exemplo:

É como uma rotina de inicialização que é chamada sempre que um novo objeto é criado.
Um construtor pode parecer, mas não é um método. Algumas pessoas o chamam de um “método
especial”, mas, definitivamente, não o é, uma vez que não tem retorno e só é chamado durante a
construção do objeto.
26

4.7.1. Invocando outro construtor

Um construtor só pode rodar durante a construção do objeto, isto é, você nunca conseguirá
chamar o construtor em um objeto já construído. Porém, durante a construção de um objeto, você
pode fazer com que um construtor chame outro para não ter de ficar copiando e colando. Exemplo:

4.8. Atributos de classe

O banco do exemplo anterior também quer controlar a quantidade de contas existentes no


sistema. Como poderíamos fazer isso? A ideia mais simples é:

Conta c = new Conta();

totalDeContas = totalDeContas + 1;

Aqui, voltamos em um problema parecido com o da validação de CPF. Estamos


espalhando um código por toda aplicação, e quem garante que conseguiremos lembrar de
incrementar a variável “totalDeContas” toda vez? Tentamos, então, passar para a seguinte proposta:

Quando criarmos duas contas, qual será o valor do “totalDeContas” de cada uma delas?
Será 1, pois cada uma tem essa variável. O atributo é de cada objeto.

Seria interessante, então, que essa variável fosse única, compartilhada por todos os objetos
dessa classe. À vista disso, quando mudasse por meio de um objeto, o outro enxergaria o mesmo
valor. Para fazer isso em Java, declaramos a variável com a palavra-chave “static”. Exemplo:
“private static int totalDeContas”. Exemplo: “private static int totalDeContas;”.
27

Quando declaramos um atributo como “static”, ele passa a não ser mais um atributo de
cada objeto, e sim um atributo da classe. A informação fica guardada pela classe e não é mais
individual para cada objeto.

Por esta razão, para acessarmos um atributo estático, não usamos a palavra-chave “this”,
mas, sim, o nome da classe. Exemplo:

Já que o atributo é privado, como podemos acessar essa informação a partir de outra
classe? Precisamos de um getter. Exemplo:

Precisamos criar uma conta antes de chamar o método, e isto não é o ideal, pois
gostaríamos de saber quantas contas existem sem precisar ter acesso a um “objeto-conta”. A ideia
aqui é a mesma: transformar esse método “getter” que todo objeto-conta tem, em um método de
toda a classe. Novamente, usamos a palavra “static”, mudando o método anterior. Exemplo:

Métodos e atributos estáticos só podem acessar outros métodos e atributos estáticos da


mesma classe, o que faz todo sentido, dado que dentro de um método estático, não temos acesso à
referência this, pois um método estático é chamado por meio da classe, e não de um objeto.

O static realmente apresenta um comportamento procedural, porém, em muitas vezes, é


necessário.
28

4.9. Herança

Considere o seguinte caso de uma empresa que tem funcionários. Podemos modelar uma
classe Funcionário da seguinte forma:

Além de um funcionário comum, há também outros cargos, como os gerentes; estes


guardam a mesma informação que um funcionário comum, mas, também, têm outros dados e
funcionalidades um pouco diferentes. Por exemplo, um gerente no nosso banco tem uma senha
numérica que permite o acesso ao sistema interno do banco, além do número de funcionários os
quais ele gerencia. Exemplo de classe:

Poderíamos ter deixado a classe Funcionário mais genérica, mantendo nela a senha de
acesso e o número de funcionários gerenciados. Caso o funcionário não fosse um gerente,
deixaríamos esses atributos vazios.

Essa é uma possibilidade, porém, com o tempo, podemos passar a ter muito atributos
opcionais, e a classe ficaria estranha. E quanto aos métodos? A classe Gerente tem o método
autentica , que não faz sentido existir em um funcionário o qual não é gerente.

Se tivéssemos um outro tipo de funcionário que tem características diferentes do


funcionário comum, precisaríamos criar uma outra classe e copiar o código novamente!

Além disso, se um dia precisarmos adicionar uma nova informação a todos os


funcionários, precisaremos passar por todas as classes de funcionário e adicionar esse atributo. O
problema acontece novamente por não centralizarmos os dados básicos de um funcionário em um
único lugar.
29

Existe um jeito, em Java, de relacionarmos uma classe de tal maneira que uma delas herda
tudo que a outra tem. Isso é uma relação de classe mãe e classe filha chamada de herança. No nosso
caso, gostaríamos de fazer com que o Gerente tivesse tudo que um Funcionário tem, gostaríamos
que ela fosse uma extensão de Funcionário. Fazemos isso por meio da palavra-chave “extends”.
Exemplo:

Assim, sempre que instanciarmos um objeto da classe Gerente, também será instanciado
um objeto da classe Funcionário relacionado ao objeto da classe Gerente, portanto, este terá os
atributos definidos na classe Funcionário, pois um Gerente é um Funcionário. Exemplo:

Dizemos que a classe Gerente herda todos os atributos e métodos da classe mãe, no nosso
exemplo, da classe Funcionário. Para ser mais preciso, ela também herda os atributos e métodos
privados, porém não consegue acessá-los diretamente. Para acessar um membro privado na filha
indiretamente, seria necessário que a mãe expusesse um outro método visível que invocasse esse
atributo ou método privado.

Assim, a nomenclatura mais encontrada é que “Funcionário é a superclasse de Gerente, e


Gerente é a subclasse de Funcionário”. Dizemos também que todo “Gerente é um Funcionário, mas
nem todo Funcionário é um Gerente”. Outra forma é dizer que “Funcionário é a classe mãe de
Gerente, e Gerente é a classe filha de Funcionário”.
30

E se precisamos acessar os atributos que herdamos? Não gostaríamos de deixar os atributos


de Funcionário com modificadores de acesso (ou visibilidade) “public”, pois, dessa maneira,
qualquer um poderia alterar os atributos dos objetos desse tipo, e desrespeitaria o Encapsulamento.

Então, para solucionar este problema, existe um outro modificador de acesso chamado
“protected”, o qual fica entre o “private” e o “public”. Um atributo “protected” só pode ser acessado
(é visível) pela própria classe, suas subclasses e classes encontradas no mesmo pacote. Exemplo:

É importante conhecer a regra em que, uma classe pode ter várias filhas, mas, cada uma
delas, apenas uma mãe. É a chamada herança simples do Java. Veja o esquemático apresentado na
figura abaixo:

4.10. Override

No Java, quando herdamos um método, podemos alterar seu comportamento. Ou seja,


podemos reimplementar (sobrescrever, reescrever ou “override”) esse método. Considere, ainda, o
exemplo anterior.

Todo fim de ano, os funcionários do banco recebem uma bonificação. Os funcionários


comuns recebem 10% do valor do salário e os gerentes, 15%. Assim, para os funcionários, a classe
Funcionário precisaria de um novo método. Exemplo:
31

Se deixarmos a classe Gerente como está, ela herdará o método, e o resultado aqui será 500
reais para funcionários e gerentes. Não queremos essa resposta, pois o gerente deveria ter 750 reais
de bônus, nesse caso. Para consertar isso, uma das opções seria criar um método na classe Gerente
chamado, por exemplo, “getBonificacaoDoGerente”. O problema é que teríamos dois métodos em
Gerente, confundindo bastante quem for usar essa classe, além disso, cada um dá uma resposta
diferente.

Então, graças à possibilidade de sobrescrever (override) o método “getBonificacao”,


podemos resolver este problema conforme apresenta a imagem em anexo:

4.10.1. A anotação @Override

Há como deixar explícito no seu código que determinado método é a reescrita (ou
sobrescrita) de um método da classe mãe. Podemos fazê-lo colocando a expressão “@Override”
acima do método. Isso é chamado anotar o método. Existem diversas anotações, e cada uma terá
um efeito diferente sobre seu código. Exemplo:

Repare que, por questões de compatibilidade, isso não é obrigatório, mas, caso um método
esteja anotado com “@Override”, ele necessariamente precisa reescrever um método da classe mãe,
32

ou compilador indicará um erro.

4.10.2. Invocando o método superior

Depois de reescrito, não podemos mais chamar o método antigo que foi herdado da classe
mãe: realmente alteramos o seu comportamento, mas podemos invocá-lo no caso de estarmos
dentro da classe.

Imagine que, para calcular a bonificação de um Gerente, devamos fazer igual ao cálculo de
um Funcionário, porém adicionando R$ 1000. Poderíamos fazer assim:

Porém, teríamos um problema: o dia em que o método “getBonificacao” do Funcionário


mudar, precisaremos mudar o método do Gerente a fim de acompanhar a nova bonificação. Para
evitar isso, o método “getBonificacao” do Gerente pode invocar o método do “getBonificacao” do
Funcionário utilizando a palavra-chave “super”. Exemplo:

4.11. Polimorfismo

O que guarda uma variável do tipo Funcionário? Uma referência para um Funcionário,
nunca o objeto em si.
33

Na herança, vimos que todo Gerente é um Funcionário, pois é uma extensão deste, mas
nem todo funcionário é um gerente. Assim, podemos referenciar um objeto Gerente a partir de uma
referência de um objeto da classe Funcionário. Esta é a semântica da herança. Exemplo:

Polimorfismo é a capacidade de um objeto poder ser referenciado de várias formas, mas


cuidado: polimorfismo não quer dizer que o objeto se transforma em outro, muito pelo contrário,
um objeto nasce de um tipo e morre daquele tipo, o que pode mudar é a maneira como nos
referimos a ele.

Considerando o exemplo de código da figura anterior, se acrescentarmos a seguinte linha


de código: “[Link]();”, qual será o resultado apresentado? 500 reais ou 750
reais?

No Java, a invocação de método sempre será decidida em tempo de execução, assim, a


JVM buscará o objeto pela sua referência na memória, portanto, como foi instanciado, e executará o
método correto. Ou seja, sempre o relacionando com a sua instância, e não com a que estamos
usando para referenciá-lo. Neste exemplo, apesar de estarmos nos referenciando a um objeto da
classe Gerente com uma referência de um objeto da classe Funcionário, o método executado é o do
Gerente, portanto, o retorno é 750.

Parece estranho criar um gerente e referenciá-lo como apenas um funcionário. Por que
faríamos isso? Na verdade, a situação que costuma aparecer é a que temos um método que recebe
um argumento do tipo Funcionário. Exemplo:
34

E, portanto, em algum lugar da aplicação, pode ocorrer o seguinte código abaixo:

Repare que conseguimos passar um Gerente para um método que recebe um Funcionário
como argumento. Pense em uma porta na agência bancária com o seguinte aviso: “Permitida a
entrada apenas de funcionários”. Um gerente pode passar nessa porta? Sim, pois Gerente é um
Funcionário.

Qual será o valor resultante? Não importa que dentro do método registra o
“ControleDeBonificacoes” receba Funcionário. Quando ele receber um objeto que realmente é um
Gerente, o seu método reescrito será invocado. Reafirmando: não importa como nos
referenciamos a um objeto, o método a ser invocado é sempre o do próprio objeto.

A grande vantagem é que, no dia em que criarmos uma classe Secretaria, por exemplo, que
também será filha de Funcionário, precisaremos mudar a classe de “ControleDeBonificacoes”?
Não. Basta a classe Secretaria reescrever os métodos que lhe parecerem necessários. É exatamente
esse o poder do polimorfismo com a reescrita de método: diminuir o acoplamento entre as classes
para evitar que novos códigos resultem em modificações em inúmeros lugares.

Ou seja: quem criou “ControleDeBonificacoes” pode nunca ter imaginado a criação da


classe Secretaria ou Engenheiro. Contudo, não será necessário reimplementar esse controle em cada
nova classe: reaproveitamos aquele código.

4.11.1. Herança versus Acoplamento

Note que o uso de herança aumenta o acoplamento entre as classes, isto é, o quanto uma
35

classe depende de outra. A relação entre as classes mãe e filha é muito forte e isso acaba fazendo
com que o programador das classes filhas tenha de conhecer a implementação da classe mãe, e vice-
versa, portanto, fica difícil fazer uma mudança pontual no sistema.

Por exemplo, imagine se mudássemos algo na nossa classe Funcionário, mas não
quiséssemos que todos os funcionários sofressem a mesma mudança. Precisaríamos passar por cada
uma das filhas de Funcionário, verificando se ela se comporta como deveria, ou se deveríamos
sobrescrever o tal método modificado.

Esse é um problema da herança, e não do polimorfismo, que resolveremos mais tarde com
a ajuda de Interfaces.

5. Pacotes – suas classes e bibliotecas TESTE!!!


Pensando um pouco mais, notamos a existência de um outro problema e da própria
solução: o sistema operacional não permite a existência de dois arquivos com o mesmo nome sob o
mesmo diretório, portanto precisamos organizar nossas classes em diretórios diferentes.

Os diretórios estão diretamente relacionados aos chamados pacotes e costumam agrupar


classes de funcionalidades similares ou relacionadas.

Podemos ter um diretório chamado “java” e, dentro dele, ter outro chamado “util”. Para
referenciá-lo no programa, podemos usar a nomenclatura: “[Link]” onde estão as classes “Date,”
“SimpleDateFormat” e “GregorianCalendar”, etc.; todas elas trabalham com datas de formas
diferentes. Desta forma, podemos chamar “[Link]” de pacote. Exemplo:

5.1. Diretórios

Se a classe Cliente está no pacote “contas”, ela deverá estar no diretório com o mesmo
nome: contas. Se este diretório se localiza dentro do diretório “caelum”, que se localiza dentro do
“com” e, finalmente, dentro do “br”, referenciamos a classe no programa por “pacote”, da seguinte
forma: “[Link]”, o que significa que está no diretório “br/com/caelum/contas”.
36

A classe Cliente que se localiza nesse último diretório mencionado deve ser escrita da
seguinte forma:

Fica fácil notar que a palavra-chave package indica qual o pacote/diretório contém essa
classe. Um pacote pode conter nenhum, ou mais subpacotes e/ou classes dentro dele.

5.2. Import

Para usar uma classe do mesmo pacote, basta fazer referência a ela como foi feito até
agora, simplesmente escrevendo o próprio nome da classe. Se quisermos que uma classe Banco e
uma Cliente fiquem dentro do pacote “[Link]”, elas devem ser declaradas assim:

Note que nenhum modificador de acesso, como “private”, public”, “protected”, etc. Foi
declarado para ambas classes Banco e Cliente. Assim, “default” (ou package) é um modificador um
pouco menos restritivo que o “private”. Este tipo de modificador é aplicado a todas as classes,
atributos ou métodos, os quais, não tiveram o seu modificador explicitamente declarado. Este
modificador permite que apenas classes do mesmo pacote tenham acesso as propriedades que
possuem este modificador. Exemplo:
37

Repare que precisamos referenciar a classe Banco com todo o nome do pacote na sua
frente. Esse é o conhecido “Fully Qualified Name” de uma classe. Em outras palavras, é o
verdadeiro nome de uma classe, por isso duas classes com o mesmo nome em pacotes diferentes
não entram em conflito.

Mesmo assim, ao tentar compilar a classe anterior, surge um erro reclamando que a classe
Banco não está visível, e isto tem a ver diretamente com o que estudamos sobre o modificador de
acesso padrão “package”: as classes Banco e Cliente são acessíveis apenas a outras classes dentro
do mesmo pacote.

Então, para permitir que a classe “TesteDoBanco” acesse a classe Banco em outro pacote,
precisamos alterar esta última e transformá-la em pública. Exemplo:

Voltando ao código do “TesteDoBanco”, é necessário escrever todo o pacote para


identificar qual classe queremos usar? O exemplo que usamos ficou bem complicado de ler:

Existe uma maneira mais simples de se referenciar à classe Banco: basta importá-la do
pacote “[Link]”. Exemplo:

Ou seja: isto faz com que não precisemos referenciar ao utilizar o “Fully Qualified Name”,
podendo usar a classe Banco dentro do nosso código em vez de escrever o longo
“[Link]”.

Dois pontos importantes sobre a importação de pacotes:

1. É muito importante manter a ordem! Primeiro, aparece uma (ou nenhuma) vez o “package”,
38

depois pode aparecer um ou mais “imports” e, por último, as declarações de classes; e

2. É possível importar um pacote inteiro (todas as classes do pacote, exceto os subpacotes) por
meio do coringa ‘*’. Exemplo: “import [Link].*;”.

Importar todas as classes de um pacote não implica perda de performance em tempo de


execução, mas pode trazer problemas com classes de mesmo nome. Além disso, importar de um em
um é considerado boa prática, pois facilita a leitura a outros programadores.

5.3. Pacote [Link]

Já usamos, por diversas vezes, as classes String e System. Vimos o sistema de pacotes do
Java e nunca precisamos usar a cláusula import para estas classes. Isso ocorre porque elas estão
dentro do pacote [Link] que é, automaticamente, importado. É o único pacote com essa
característica.

5.3.1. Classe System

A classe System possui uma série de atributos e métodos estáticos. Já usamos o atributo
[Link] para imprimir.

Verificando a documentação, é possível que o atributo “out” é do tipo PrintStream do


pacote [Link]. Já podemos perceber que poderíamos quebrar o “[Link]” em duas linhas.
Exemplo:

O System conta também com um método que desliga a Java Virtual Machine e retorna um
código de erro para o sistema operacional. Esse método é o exit. Exemplo: “[Link](0)”.

5.3.2. Pacote [Link]

Em todo método no qual precisamos receber algum parâmetro, temos que declarar o seu
tipo. Por exemplo, no nosso método “saca”, precisamos passar como parâmetro um valor do tipo
double. Se tentarmos passar qualquer coisa diferente disso, teremos um erro de compilação.

Para entender o que está acontecendo, consideraremos um método que recebe uma Conta.
Exemplo:
39

Esse método pode ser invocado passando como parâmetro qualquer tipo de conta que
temos no nosso sistema: ContaCorrente e ContaPoupanca, pois ambas são filhas da classe Conta. Se
quiséssemos que o nosso método conseguisse receber qualquer tipo de objeto, teríamos de ter uma
classe a qual fosse mãe de todos esses objetos. É para isso que existe a classe Object.

Quando o Java não encontra a palavra-chave extends, ele considera que você está herdando
da classe Object, que também se encontra dentro do pacote [Link]. Você até mesmo pode
escrever essa herança, explicitamente:

Podemos também afirmar que qualquer objeto em Java é um Object e pode ser
referenciado como tal. Então qualquer objeto tem todos os métodos declarados na classe Object,
como veremos os dois principais nos subcapítulos seguintes.

[Link]. Método toString

A habilidade de poder se referir a qualquer objeto como Object nos oferece muitas
vantagens. Podemos criar um método que recebe um Object como argumento, isto é, qualquer
objeto! Por exemplo, o método println poderia ser implementado da seguinte maneira:

Dessa forma, qualquer objeto que passarmos como parâmetro poderá ser impresso no
console, desde que ele tenha o método toString. Para garantir que todos os objetos do Java tenham
este método, ele foi implementado na classe Object.

Por padrão, o método toString do Object retorna, concatenados:

1. O nome da classe;

2. Um símbolo @; e

3. Um número de identidade.

Vejamos um exemplo: Conta@34f5d74a.

Mas e se quisermos imprimir algo diferente? Na nossa tela de detalhes de conta, temos
40

uma caixa de seleção na qual as nossas contas estão sendo apresentadas com o valor do padrão do
toString. Sempre que queremos modificar o comportamento de um método em relação à
implementação herdada da superclasse, podemos sobrescrevê-lo na classe filha. Exemplo:

Agora podemos invocar este método explicita ou implicitamente, conforme apresenta a


imagem abaixo:

[Link]. Método equals

Até agora, estamos ignorando o fato que podemos ter mais de uma conta de mesmo
número e agência no sistema. Atualmente, quando inserimos uma nova conta, o sistema verifica se a
conta inserida é igual a alguma outra conta já cadastrada, mas qual critério de igualdade é utilizado
por padrão para fazer essa verificação?

Assim como no caso do toString, todos objetos do Java têm um outro método chamado
equals que é utilizado para comparar objetos daquele tipo. Por padrão, esse método apenas
compara as referências dos objetos.

Ou seja, como toda vez que inserimos uma nova conta no sistema estamos fazendo “new”
em algum tipo de conta, portanto, as referências nunca vão ser iguais, mesmo que os dados (número
e agência) tenham os mesmos valores de uma conta para outra.

E se fosse preciso comparar os atributos? Quais atributos ele deveria comparar? O Java,
por si só, não faz isso, mas podemos sobrescrever o equals da classe Object para criarmos esse
critério de comparação.

O equals recebe um Object como argumento e deve verificar se ele mesmo é igual ao
Object recebido para retornar um boolean. Se você não reescrever esse método, o comportamento
41

herdado é fazer um == com o objeto recebido como argumento. Exemplo de “override”:

[Link]. Casting de referências

No momento em que recebemos uma referência a um Object, como acessaremos os


métodos e atributos desse objeto que imaginamos ser uma Conta se estamos referenciando-o como
Object? Não podemos acessá-lo sendo Conta. O código anterior não compila.

Poderíamos, então, atribuir essa referência de Object a uma da classe Conta para, depois,
acessarmos os atributos necessários? Tentemos o seguinte: Conta outraConta = object.

A linha em vermelho e acima não compila, pois nem todo Object é uma Conta. Nós temos
certeza de que esse Object se refere a uma Conta, uma vez que a nossa lista só imprime contas, mas
o compilador Java não tem garantia disso.

A fim de realizar essa atribuição, devemos avisar o compilador Java que realmente
queremos fazer isso, sabendo do risco que corremos. Façamos o casting de referências parecido
com o de tipos primitivos. Exemplo: Conta outraConta = (Conta) object.

Esse código roda sem nenhum problema, pois, em tempo de execução, a JVM verificará se
essa referência realmente é a um objeto do tipo Conta. Se não fosse, uma exceção do tipo
“ClassCastException” seria lançada.

Com isso, nosso método equals ficaria conforme apresenta a figura abaixo:
42

Pelo contrato definido pela classe Object, devemos retornar false também, contanto que o
objeto passado não seja de tipo compatível com a sua classe. Então, antes de fazer o casting,
devemos verificar isso e, para tal, usamos a palavra-chave instanceof, ou teríamos uma exception
sendo lançada. Assim, podemos resumir nosso equals de tal forma a não usar um bloco “if”.
Exemplo:

Você poderia criar um método com outro nome em vez de reescrever equals, que recebe
Object, mas o equals é importante, pois muitas bibliotecas o chamam mediante ao polimorfismo.

O método hashCode() anda de mãos dadas com o método equals() e é de fundamental


entendimento no caso de você utilizar suas classes com estruturas de dados que usam tabelas de
espalhamento, como algumas colections.

5.3.3. Pacote [Link]

String é uma classe em Java. Variáveis do tipo String guardam referências a objetos, e não
um a valor, como acontece com os tipos primitivos. Por esta razão, é possível criar uma string com
a palavra reservada “new”. Exemplos:
43

[Link]. Método “equals”

Criamos, no exemplo anterior, dois objetos diferentes. O que acontece quando comparamos
essas duas referências utilizando o “==”?

Como o “==” compara referências, o bloco “else” é o que será executado. Ou seja: temos
aqui dois objetos distintos! Então, como faríamos para verificar se o conteúdo do objeto é o
mesmo? Utilizamos o método equals que foi reescrito pela classe String, com o objetivo de fazer a
comparação de “char” em “char”. Exemplo:

Aqui, a comparação retorna verdadeiro, pois quem implementou a classe String decidiu
que este seria o melhor critério de comparação. Você pode descobrir os critérios de igualdade de
cada classe pela documentação.

[Link]. Método “compareTo”

Também podemos utilizar o método “compareTo” para comparar duas Strings. Ele também
pertence a um objeto String e recebe uma String como argumento, comparando cada caractere
Unicode, e devolve:

1. Zero se forem iguais;

2. Negativo se a segunda (a do argumento) for maior; e

3. Positivo se a String propriamente for maior que a do argumento.

Ou seja: são contados os caracteres Unicode de cada String e é efetuada uma operação de
subtração, retornando conforme indicado acima.
44

[Link]. Concatenção

Podemos também concatenar Strings usando o ‘+’, além de concatenar Strings com
qualquer objeto, e até números. Exemplo:

O compilador utilizará os métodos apropriados da classe String e, possivelmente, métodos


de outras classes para realizar tal tarefa.

[Link]. Imutável

Fato importante: uma String é imutável. O Java cria um pool de Strings para usá-lo como
cache. Se a String não fosse imutável, mudar o valor de uma significaria afetar todas as Strings de
outras classes que tivessem o mesmo valor. Considere o seguinte exemplo:

Pode parecer estranho, mas ele imprime "fj11" em minúsculo. Todo método que parece
alterar o valor de uma String, na verdade, cria uma nova String com as mudanças solicitadas e a
retorna! Tanto que o método “,toUpperCase()” não é void. O código realmente útil ficaria conforme
apresenta a figura abaixo:

Para realmente alterar o valor da variável “palavra”, seria necessário o código conforme
apresenta a outra figura, abaixo:

Isso funciona da mesma forma para todos os métodos que parecem alterar o conteúdo de
uma String. Se você ainda quiser trocar o número 1 para 2, faríamos conforme o exemplo
apresentado na figura abaixo:
45

Ou, ainda, poderíamos concatenar as invocações de método, já que uma String é devolvida
a cada invocação. Exemplo:

[Link]. Outros métodos

Existem diversos métodos da classe String que são extremamente importantes. Abaixo,
mais alguns exemplos:

1. “charAt(i)”: retorna o caractere existente na posição i da String;

2. “length()”: retorna o número de caracteres na mesma posição;

3. substring(i) recebe um int e devolve a subString a partir da posição passada por esse int;

4. “indexOf”: recebe um char ou uma String e devolve o índice em que aparece, pela primeira
vez, na String principal;

5. “lastIndexOf ”: devolve o índice da última ocorrência;

6. “toUpperCase”: devolve uma nova String toda em maiúscula;

7. “toLowerCase”: devolve uma nova String toda em minúscula;

8. i“sEmpty”: devolve “true” se a String for vazia, ou “false”, caso contrário;

9. “contains”: pesquisar; e

10. “matches”:

[Link]. Stringbuffer e Stringbuilder

Como a classe String é imutável, trabalhar com uma mesma String diversas vezes pode ter
um efeito colateral: gerar inúmeras Strings temporárias. Isso prejudica a performance da aplicação
consideravelmente.

Se, porventura, você trabalhar muito com a manipulação de uma mesma String (por
exemplo, dentro de um laço), o ideal é utilizar a classe StringBuffer, pois esta representa uma
46

sequência de caracteres e, diferentemente da String, ela é mutável e não tem aquele pool.

A classe StringBuilder tem exatamente os mesmos métodos, com a diferença de ela não ser
thread-safe. Esse conceito está descrito no capítulo apêndice de [Link].

6. JAVADOC
No site da Oracle, você pode (e deve) baixar a documentação das bibliotecas do Java,
frequentemente, referida como Javadoc ou API (sendo na verdade a documentação da API). Uma
documentação está disponível como uma página HTML, conforme o exemplo abaixo:

Nesta documentação, no quadro superior esquerdo, você encontra os pacotes e, no inferior


esquerdo, está a listagem das classes e interfaces do respectivo pacote (ou de todos, caso nenhum
tenha sido especificado). Ao clicar em uma classe ou interface, o quadro da direita passa a detalhar
todos atributos e métodos.

Repare que métodos e atributos privados não estão aí. O importante é documentar o que
sua classe faz, e não como ela o faz: detalhes de implementação, como atributos e métodos
privados, não interessam às pessoas desenvolvedoras que usarão a sua biblioteca (ou, ao menos, não
deveriam interessá-las). Você também consegue gerar esse Javadoc a partir da linha de comando
digitando: “javadoc [Link]”.

Existem tags (ou anotações) que são inseridas dentro do bloco de comentários, antecedidas
pelo símbolo ‘@’ (arroba) e, após o nome da própria tag, inseri-se o conteúdo desejado. Veja no
quadro abaixo as tags disponíveis pelo JavaDoc:
47

Para colocarmos comentários na documentação, devemos adicionar o texto ao código sob


forma de comentário, abrindo-o com /** e fechando-o com */ e, nas outras linhas, apenas colocando
‘*’. Também podemos definir outras informações nesse texto, como: autor, versão, parâmetros,
retorno, etc. Adicione alguns comentários ao seu projeto como abaixo:

Ou adicione alguns comentários em algum método seu. Exemplo:

7. Classes abstratas
Para entendermos o uso de classes abstratas, vejamos um outro caso em que não faz
48

sentido ter um objeto de um tipo, apesar de a classe existir: imagine uma classe Pessoa e duas filhas,
“PessoaFísica” e “PessoaJurídica”. Quando puxamos um relatório de nossos clientes (um array de
Pessoa, por exemplo), queremos que cada um deles seja apenas ou uma “PessoaFísica”, ou uma
“PessoaJurídica”. A classe Pessoas, nesse caso, estaria sendo usada apenas para ganhar o
polimorfismo e herdar algumas coisas: não faz sentido permitir instanciá-la. Para resolver esses
problemas, temos as classes abstratas.

Voltando ao exemplo anterior, o que, exatamente, vem a ser a nossa classe “Funcionário”?
Nossa empresa tem apenas Diretores, Gerentes, Secretárias, etc., portanto, ela é uma classe que
apenas idealiza um tipo, define somente um rascunho e não deve ser instanciada.

Ou seja, para o nosso sistema, é inadmissível um objeto ser apenas do tipo Funcionário, até
pode existir um sistema em que faça sentido ter objetos do tipo Funcionário ou apenas Pessoa, mas,
no nosso caso, não.

Assim, utilizamos a palavra-chave “abstract” para impedir que ela possa ser instanciada.
Esse é o efeito direto de se usar o modificador “abstract” na declaração de uma classe. Exemplo:

Ao tentar instanciar um objeto desta classe, o seguinte erro ocorrerá, conforme apresenta a
figura abaixo:

O código acima não compila, pois o problema é instanciar a classe – criar referência você
pode. Se ela não pode ser instanciada, para que serve? Serve para o polimorfismo e herança dos
atributos e métodos, que são recursos muito poderosos, como já vimos. Então, herdemos essa classe
reescrevendo o método “getBonificacão”. Exemplo:
49

Agora, esquemático fica diferente do proposto inicialmente, conforme apresenta a figura


abaixo:

Mas qual é a real vantagem de uma classe abstrata? Poderíamos ter feito isso com uma
herança comum. Por enquanto, a única diferença é que não podemos instanciar um objeto do tipo
Funcionário, que já é de grande valia, dando mais consistência ao sistema.

Fique claro que a nossa decisão de transformar a classe Funcionario em uma abstrata
dependeu do nosso domínio. Pode ser que, em um sistema com classes similares, faça sentido uma
classe análoga a Funcionário ser concreta.

7.1. Métodos abstratos

Se o método “getBonificacão” não fosse reescrito, ele seria herdado da classe mãe, fazendo
com que devolvesse o salário mais 20%.

Levando em consideração que cada funcionário em nosso sistema tem uma regra
totalmente diferente a fim de ser bonificado, faz algum sentido ter esse método na classe
Funcionário? Será que existe uma bonificação padrão para todo tipo de Funcionário? Parece que
não, pois cada classe filha terá um método diferente de bonificação e, de acordo com nosso sistema,
não existe uma regra: queremos que cada pessoa a qual escreve a classe de um Funcionário
diferente (subclasses de Funcionário) reescreva o método “getBonificacão” de acordo com as suas
regras.

Em Java, existe um recurso no qual, em uma classe abstrata, podemos escrever que
determinado método deverá, sempre, ser reescrito pelas classes filhas. Isto é, um método abstrato.

Ele indica que todas as classes filhas (concretas) devem reescrever esse método, ou não
compilarão. É como se você herdasse a responsabilidade de ter aquele método.
50

7.1.1. Declarar um método abstrato

Às vezes, não fica claro como declarar um método abstrato, mas basta escrever a palavra-
chave “abstract” na sua assinatura e colocar um ponto e vírgula em vez de abrir e fechar chaves.
Exemplo:

Repare que não colocamos o corpo do método e usamos a palavra-chave “abstract” para
defini-lo. Por que não colocar corpo algum? Porque esse método nunca será chamado. Sempre que
alguém chamar o método “getBonificacão”, invocará, na verdade, o método implementado em uma
das suas filhas a qual realmente escreveu o método.

Qualquer classe que estender a classe Funcionário será obrigada a reescrever esse método,
tornando-o concreto. Se não o reescreverem, um erro de compilação ocorrerá.

Outro exemplo seria conforme apresenta o diagrama de classes abaixo:

Ou seja, temos:

1. A classe abstrata Funcionário com o método abstrato “getBonificacão”;

2. As classes Gerente e Presidente estendem Funcionário e implementam o método


“getBonificacão”; e

3. A classe Diretor, que estende Gerente, mas não implementa o método “getBonificacão”.
51

Essas classes compilarão? A resposta é sim e, além de tudo, farão exatamente o que nós
queremos, pois quando Gerente e Presidente têm os métodos perfeitamente implementados, a classe
Diretor, a qual não os tem, usará a implementação herdada de Gerente.

Considere outro exemplo mais elaborado, conforme apresentado no diagrama de classes,


na figura abaixo:

No diagrama acima, incluímos uma classe abstrata Secretaria sem o método


“getBonificacao”, a qual é estendida por mais duas classes (“SecretariaAdministrativa”,
“SecretariaAgencia”) que, por sua vez, implementam o método “getBonificacão”, compilará?
Rodará?

De novo, a resposta é sim, pois Secretaria é uma classe abstrata, apesar de herdar outra
classe abstrata. Por isso, o Java tem certeza de que ninguém conseguirá instanciá-la e, tampouco,
chamar o seu método “getBonificacão”. Lembrando que, nesse caso, não precisamos nem ao menos
escrever o método abstrato “getBonificacão” na classe Secretaria.

8. Interfaces
Imagine que um sistema de controle do banco possa ser acessado pelos diretores do banco,
além dos gerentes, conforme apresenta o diagrama de classes abaixo:
52

As classes Diretor e Gerente são implementadas conforme a figura abaixo:

Repare que o método de autenticação de cada tipo de Funcionário pode variar muito. Para
isto, vamos considerar uma classe chamada “SistemaInterno” e seu controle: receber um Diretor ou
Gerente como argumento, verificar se ele se autentica e colocá-lo dentro do sistema. Exemplo:

Conforme a figura acima, a classe “SistemaInterno” aceita qualquer tipo de Funcionário,


mas nem a todos será permitido acesso ao sistema, portanto, alguns não podem ter acesso ao
método autentica. Isso nos impede de chamar esse método com uma referência da classe
Funcionário (haveria um erro de compilação).

Uma possibilidade é criar dois métodos de login na classe “SistemaInterno”: um para


receber Diretor e outro para Gerente. Exemplo:
53

Já vimos que essa não é uma boa escolha, pois, cada vez que criarmos uma nova classe de
Funcionário que é autenticável, precisaríamos adicionar um novo método de login na classe
“SistemaInterno”.

IMPORTANTE: em Java, métodos com o mesmo nome, mas parâmetros diferentes são
permitidos desde que não sejam ambíguos, isto é, que exista uma maneira de distingui-los no
momento da chamada. Isso se chama sobrecarga de método, ou Overloading. Não confundir com
sobrescrever método, ou Overriding, que é um conceito muito mais poderoso.

Uma outra situação que acrescentaria complexidade ao sistema, é caso de que todos os
clientes também tenham acesso à classe “SistemaInterno”, mas Cliente definitivamente não é um
Funcionário e não deve ter acesso, por exemplo, a um método “getBonificacão”, a um atributo
salário e outros membros que não fazem o menor sentido para aquela classe.

O que precisamos para resolver este problema? Arranjar uma forma de poder referenciar
um objeto da classe Diretor, da classe Gerente e da classe Cliente de uma mesma maneira, isto é,
achar um fator comum.

Se houvesse uma forma na qual essas classes garantissem a existência de um determinado


método por meio de um contrato, resolveríamos o problema.

Assim, podemos criar este "contrato" o qual define tudo o que uma classe deve fazer se
quiser ter um determinado status. Exemplo:

O estabelecimento de um contrato chama-se interface, pois é a maneira pela qual


poderemos interagir com uma classe estabelecendo padrões de interação. Ou seja: interface é a
maneira estabelecida por meio da qual interagimos com um objeto. Exemplo de interface:
54

Quem quiser pode assinar esse contrato, sendo, assim, obrigado a implementar como será
feita essa autenticação. A vantagem é que, se um Gerente assinar esse contrato, podemos nos referir
a um objeto da classe Gerente como um “Autenticavel”.

Lemos a interface da seguinte maneira: “quem desejar ser “Autenticavel” precisa saber
autenticar recebendo um inteiro e retornando um booleano”. Ela é um contrato que quem assina se
responsabiliza por implementar esses métodos (cumprir o contrato).

Ou seja, a ideia base de uma interface é ela definir uma série de métodos, mas nunca conter
suas implementações. Ela só expõe o que o objeto deve fazer, e não como ele o faz nem o que ele
tem. Como ele o faz será definido em uma implementação dessa interface. Esta caratcterística já foi
estudada em classes e métodos abstratos, mas podemos notar que, em Interfaces, não precisamos
declarar os métodos como abstratos, isto porque todas as declarações de métodos em uma interface
são, automaticamente, abstratas.

E o Gerente pode “assinar” o contrato, ou seja, implementar a interface. No momento em


que ele implementa essa interface, precisa escrever os métodos pedidos por ela (muito parecido com
o efeito de herdar métodos abstratos, aliás, métodos de uma interface são públicos e abstratos
sempre). Para implementar, usamos a palavra-chave “implements” na classe. Exemplo:

O diagrama de classes que corresponde a utilização da interface neste exemplo é


apresentado na figura abaixo:
55

A cláusula “implements” pode ser lida da seguinte maneira: “a classe Gerente se


compromete a ser tratada como Autenticavel, sendo obrigada a ter os métodos necessários,
definidos neste contrato”.

A partir de agora, podemos tratar um Gerente como sendo um Autenticavel. Ganhamos


mais polimorfismo! Temos mais uma forma de referenciar a um Gerente, pois quando crio uma
variável do tipo Autenticavel, estou criando uma referência a qualquer objeto de uma classe que
implemente Autenticavel, direta ou indiretamente. Exemplo:

Novamente, a utilização mais comum seria receber por argumento, como no nosso
SistemaInterno. Exemplo:

No dia em que tivermos mais um funcionário com acesso ao sistema, bastará que ele
implemente essa interface para se encaixar no sistema.

Qualquer Autenticavel passado ao SistemaInterno será aceitável. Repare que pouco


importa quem o objeto referenciado realmente é, pois ele tem um método “autentica” o qual é
necessário para nosso SistemaInterno funcionar corretamente. Aliás, qualquer outra classe que
futuramente implemente essa interface poderá ser passada como argumento aqui. O esquemático
abaixo apresenta esta lógica de implementação:
56

Esta lógica proporciona um desacoplamento imenso entre as classes.

A maneira pela qual os objetos se comunicam em um sistema orientado a objetos é muito


mais importante do que como eles executam. O que um objeto faz é mais importante do que como
ele o faz. Aqueles que seguem essa regra terão sistemas mais fáceis de manter e modificar.
Conforme você já percebeu, essa é uma das ideias principais que queremos passar e,
provavelmente, a mais importante de toda esta apostila.

Diferentemente das classes, uma interface pode herdar de mais de uma interface, mas
nunca implementar outra interface. É como um contrato o qual depende que outros contratos sejam
fechados antes daquele valer. Você não herda métodos e atributos, mas, sim, responsabilidades.

Um outro recurso em interfaces são os métodos default a partir do Java 8. Você pode, sim,
declarar um método concreto utilizando a palavra default ao lado, e suas implementações não
precisam necessariamente reescrevê-lo. Pesquisar métodos padrão.

9. Exceções e controle de erros


Uma exceção representa uma situação que, normalmente, não ocorre e é algo de estranho
ou inesperado no sistema. Considere o código de exemplo abaixo:
57

O sistema de exceções do Java funciona da seguinte maneira: quando uma exceção é


lançada (throw), a JVM entra em estado de alerta e verificará se o método atual toma alguma
precaução ao tentar executar esse trecho de código. Como podemos ver, o metodo2 não toma nenhuma
medida diferente do que vimos até agora.

Como o metodo2 não está tratando desse problema, a JVM para a sua execução anormalmente
sem esperá-lo terminar e volta um stackframe para baixo, em que será feita nova verificação: “o
metodo1 está se precavendo de um problema chamado NullPointerException?”, “Não...” Volta para o
main, em que também não há proteção. Então, a JVM para (na verdade, quem para é apenas a Thread
corrente).

9.1. Unchecked

Podemos iniciar indicando que RuntimeException é a exception mãe de todas as


exceptions unchecked.

Os erros em Java são um tipo de exceção que também pode ser tratado. Eles representam
problemas na máquina virtual e não devem ser tratados em 99% dos casos, já que, provavelmente, o
melhor a se fazer é deixar a JVM encerrar (ou apenas a Thread em questão).

Obviamente, aqui estamos forçando este caso e não faria sentido tomarmos cuidado com ele. É
fácil arrumar um problema desses: basta verificar antes de chamar os métodos se a variável está com
referência nula.

Porém, para entender o controle de fluxo de uma Exception (o erro), colocaremos o código que
vai tentar (try) executar o bloco perigoso e, caso o problema seja do tipo “NullPointerException”, ele
será pego (caught). Repare que é interessante que cada exceção no Java tenha um tipo. Ela pode ter
58

atributos e métodos. Exemplo:

Os dois primeiros códigos ocorrem no escopo do método2, onde, no primeiro, o bloco


“try...catch” contém o laço “for” inteiro, o que não é preciso. Então, no segundo código, o bloco
“try...catch” foi mais preciso e contém apenas a parte crítica. Ainda é possível, como explicado antes,
delegar o tratamento desta exceção a quem invocou o método2, que foi o método1, portanto, o bloco
“try...catch” foi realocado conforme o exemplo final, à direita.

Por fim, ainda é possível delegar o bloco “try...catch” ao main, o método de execução principal
que invocou o método1, conforme o exemplo abaixo:
59

Repare que, a partir do momento que uma “exception” foi “catched” (pega, tratada,
“handled”), a execução volta ao normal.

Em todos os casos, esses problemas, provavelmente, poderiam ser evitados por quem
programa, com blocos “if...else”, por exemplo. Repetindo: é por esse motivo que o Java não obriga a
implementar o bloco “try...catch” nessas “exceptions”, e a essas atribuímos o nome de unchecked.

9.2. Checked

Fica claro com os exemplos de código acima: não é necessário declarar que você está
tentando fazer algo onde um erro possa ocorrer. Os dois exemplos, com ou sem o “try...catch”,
compilaram e rodaram. Em um, o erro terminou o programa, e em outro, foi possível tratá-lo.

Mas não é só esse tipo de exceção que existe em Java. Um outro tipo obriga quem chama o
método ou construtor a tratar essa exceção. Chamamos esse tipo de exceção de checked, pois o
compilador checará se ela está sendo devidamente tratada, diferente das anteriores conhecidas como
unchecked.

Um exemplo interessante é o de abrir um arquivo para leitura no qual pode ocorrer o erro
do arquivo não existir. Considere o código abaixo:

O código acima não compila, e o compilador avisa que é necessário tratar o


FileNotFoundException que pode ocorrer. Exemplo:
60

Para compilar e fazer o programa funcionar, temos duas possibilidades para tratar o
problema. A primeira é tratá-lo com o bloco “try...catch” do mesmo jeito que o usamos no exemplo
anterior, de referência nula. Exemplo:

A segunda maneira de tratar esse erro é delegá-lo a quem chamou o nosso método, isto é,
passar para a frente. Exemplo:

9.3. Lançando exceções

Para o estudo de lançamento de exceções, vamos nos lembrar do método saca da nossa
classe Conta, que retorna um boolean caso consiga ou não sacar. Exemplo:

Também podemos lançar uma Exception, o que é muito útil, pois, desta maneira,
resolvemos o problema de alguém esquecer de fazer um bloco “if ” no retorno de um método.

A palavra-chave throw, que está no imperativo, lança uma Exception. Isso é bem diferente
da palavra-chave throws, que está no presente do indicativo e só avisa da possibilidade daquele
método lançá-la, obrigando o outro método que vá se utilizar daquele a se preocupar com essa
exceção em questão. Exemplo:
61

Neste exemplo, foi lançada uma exception do tipo unchecked, lembrandoque


RuntimeException é a exception mãe de todas as exceptions unchecked.

A desvantagem aqui é que ela é muito genérica; quem receber esse erro não saberá dizer
exatamente qual foi o problema. Podemos então usar uma exception mais específica. Exemplo:

IllegalArgumentException diz um pouco mais: algo foi passado como argumento que o
método não esperava. Ela é uma exception unchecked – estende de RuntimeException –, faz parte
da biblioteca do Java e é a melhor escolha quando um argumento sempre é inválido, por exemplo,
números negativos, referências nulas, etc.

Assim, o exemplo com os dois códigos: quem lança e exception e quem a captura é
apresentado na figura abaixo:

O importante sobre o exemplo apresentado na figura acima, é que, para capturar aquele
erro (ou exception), não foi usado um bloco de comando “if...else”, e sim um “try...catch”, porque
faz mais sentido, visto que a falta de saldo é uma exceção.

Podíamos melhorar ainda mais e passar para o construtor da IllegalArgumentException o


motivo da exceção. Exemplo:
62

O método getMessage() definido na classe Throwable (mãe de todos os tipos de erros e


exceptions) retornará a mensagem que passamos ao construtor da IllegalArgumentException.
Exemplo:

9.4. Criando as próprias exceções

É bem comum criar uma própria classe de exceção com o intuito de controlar melhor o uso
de suas exceções. Dessa maneira, podemos passar valores específicos para ela carregar, e que sejam
úteis de alguma forma.

Voltando ao exemplo das Contas, façamos a nossa exceção de SaldoInsuficienteException.


Exemplo:

Em vez de lançar um IllegalArgumentException, lancemos nossa própria exception com


uma mensagem "Saldo Insuficiente". Exemplo:

Podemos transformar essa Exception de unchecked para checked, obrigando quem chama
esse método a dar try-catch ou throws. Exemplo:
63

9.5. Cláusula Finally

Os blocos try e catch podem conter uma terceira cláusula chamada finally, a qual indica o
que deve ser feito após o término do bloco try ou de um catch qualquer.

É interessante colocar algo que é imprescindível de ser executado, independente se o que


você queria fazer deu certo ou não. A ocorrência mais comum é o de liberar um recurso no finally,
como um arquivo ou conexão com banco de dados, para que possamos ter a certeza de que aquele
arquivo (ou conexão) será fechado, mesmo que algo tenha falhado no decorrer do código.

No exemplo a seguir, o bloco finally será sempre executado independentemente de tudo


ocorrer bem ou de acontecer algum problema. Exemplo:

10. Arrays
Considere o seguinte exemplo: “double[] saldosDasContas;”.

O double[] é um tipo. Uma array é sempre um objeto, portanto a variável saldosDasContas


é uma referência. Precisamos criar um objeto para poder usar a array. Como criamos o objeto-
array ? Resposta: “saldosDasContas= new double[10];”.

10.1. Arrays de referências

É comum ouvirmos “array de objetos”. Porém, quando criamos uma array de alguma
classe, ela tem referências. O objeto, como sempre, está na memória principal, e, na sua array, só
ficam guardadas as referências (endereços). Exemplo:
64

Quantas contas foram criadas aqui? Na verdade, nenhuma. Foram reservados dez espaços
na memória que você pode utilizar para guardar uma referência a um objeto da classe
“ContaCorrente”, por exemplo.

Por enquanto, cada espaço não tem dado algum, ou seja: aponta para “null”. Assim, se for
executado o seguinte trecho de código como este:
“[Link](minhasContas[0].getSaldo());”, um erro durante a execução ocorrerá, pois, na
primeira posição da array, não há uma referência a um objeto de “ContaCorrente”.

IMPORTANTE: uma array de tipos primitivos armazena valores, uma array de objetos,
referências.

10.2. Percorrendo um array

Percorrer um array é como em todas as linguagens de programação, utilizando, por


exemplo, um laço de repetição “for”, mas pode ser usado o “while”, “do while”, etc.

A partir do momento que uma array foi criada, ela não pode mudar de tamanho, então, se
você precisar de mais espaço, será necessário criar um array e, antes de se referir a ele, copie os
elementos do array antigo.

10.2.1. “enhanced-for”

Caso você não tenha necessidade de manter uma variável com o índice que mostra a
posição do elemento no vetor (que é uma grande parte dos casos), podemos usar o “enhanced-for”
que equivale ao “foreach” do java. Enhanced significa “aprimorado”. Exemplo:

Ou seja, não precisamos mais do atributo “length” para percorrer matrizes cujo tamanho
não conhecemos.

Isso também é válido para arrays de referências. O “enhanced-for” nada mais é que um
“truque” de compilação para facilitar essa tarefa de percorrer arrays e torná-la mais legível.

10.2.2. Dimensões

Arrays podem ter mais de uma dimensão. Isto é, em vez de termos uma array de objetos da
65

classe Contas, podemos ter uma array de dez por dez Contas e você pode acessar a conta na posição
da coluna x e linha y. Na verdade, uma array bidimensional em Java é uma array de arrays.
Exemplo:

IMPORTANTE: um array bidimensional não precisa ser retangular, isto é, cada linha pode
ter um número diferente de colunas. Exemplo:

11. Collections
Arrays são trabalhosos de manipular e difíceis de trabalhar. Por exemplo:

1. Não podemos redimensionar um array em Java;

2. É impossível buscar diretamente por um determinado elemento cujo índice não se sabe; e

3. Não conseguimos saber quantas posições do array já foram populados sem criar, para isso,
métodos auxiliares.
66

Considere a figura abaixo:

Na figura acima, você pode ver um array que, antes, estava sendo utilizado e, depois, teve
um de seus elementos removidos.

Supondo que os dados armazenados representem contas, o que acontece quando


precisarmos inserir uma nova conta no banco? Precisaremos procurar por um espaço vazio?
Guardaremos em alguma estrutura de dados externa às posições vazias? E se não houver espaço
vazio? Teríamos de criar uma array maior e copiar os dados da antiga para ela?

Há mais questões: como posso saber quantas posições estão sendo usadas na array? Terei
sempre de percorrer o array inteiro para conseguir essa informação?

Além dessas dificuldades que os arrays apresentavam, faltava um conjunto robusto de


classes para suprir a necessidade de estruturas de dados básicas, como listas encadeadas e tabelas de
espalhamento.

Com esses e outros objetivos em mente, o comitê responsável pelo Java criou um conjunto
de classes e interfaces conhecido como Collections Framework que reside no pacote [Link].

A API de Collections é robusta e tem diversas classes que representam estruturas de dados
avançadas. Por exemplo, não é necessário reinventar a roda e criar uma lista encadeada, mas sim
utilizar aquela que o Java disponibiliza.

As coleções têm como base a interface Collection que define métodos para adicionar e
remover um elemento, além de verificar se ele está na coleção, entre outras operações, como mostra
a tabela a seguir:

Uma coleção pode implementar diretamente a interface Collection, porém, e normalmente,


67

se usa uma das duas subinterfaces mais famosas: justamente Set e List.

A interface Set define um conjunto de elementos únicos, enquanto a interface List permite
elementos duplicados, além de manter a ordem na qual eles foram adicionados.

A busca em um Set pode ser mais rápida do que em um objeto do tipo List, pois diversas
implementações se utilizam de tabelas de espalhamento (hashtables 2), realizando a busca para
tempo linear (O(1)).

IMPORTANTE: a interface Map faz parte do framework, mas não estende Collection.
Abaixo, a figura apresenta o diagrama de Colletions.

11.1. Percorrendo coleções

Como percorrer os elementos de uma coleção? Se for uma lista, podemos sempre utilizar
um laço for, invocando o método get para cada elemento. Mas e se a coleção não permitir
indexação?

Por exemplo, um Set não tem um método para pegar o primeiro, o segundo ou o quinto
elemento do conjunto, visto que um conjunto não tem o conceito de ordem.

Podemos usar o enhanced-for (o “foreach”) do Java 5 para percorrer qualquer Collection


sem nos preocupar com isso. Internamente, o compilador fará com que seja usado o Iterator da
Collection dado para percorrer a coleção. Considere o seguinte exemplo:

2 A classe Java Hashtable é um dos membros mais antigos do Java Collection Framework. É uma implementação da
estrutura de dados da tabela hash matemática. Em Java, hashtable contém internamente depósitos onde os pares de
chave/valor são armazenados. Hashtable. (…). Fonte: [Link]
68

Set<String> conjunto = new HashSet<>();

[Link]("java");

[Link]("vraptor");

[Link]("scala");

for (String palavra : conjunto) {

[Link](palavra);

Em que ordem os elementos serão acessados?

Em uma lista, os elementos aparecerão de acordo com o índice em que foram inseridos,
isto é, em concordância com o que foi pré-determinado. Em um set, a ordem depende da
implementação da interface Set: você, muitas vezes, não saberá ao certo em que ordem os objetos
serão percorridos.

Por que o Set é, então, tão importante e usado?

Para perceber se um item já existe em uma lista, é muito mais rápido usar algumas
implementações de Set do que um List, e os TreeSets já vêm ordenados de acordo com as
características que desejarmos, mas pode gerar perda de desempenho conforme a árvore ordenada
aumento, gerando, pois realiza uma inserção no tempo o(log(n)), ou seja: não linear. Sempre
considere usar um Set se não houver a necessidade de guardar os elementos em determinada ordem
e buscá-los por meio de um índice.

11.1.1. [Link]

Antes do Java 5 introduzir o novo enhanced-for, iterações em coleções eram feitas com o
Iterator. Toda coleção fornece acesso a um iterator, um objeto o qual implementa a interface Iterator,
que conhece internamente a coleção e dá acesso a todos os seus elementos, como a figura abaixo
apresenta:
69

Ainda hoje (depois do Java 5), podemos usar o Iterator, mas o mais comum é usar o
enhanced (aprimorado) for. Na verdade, o enhanced-for é apenas uma implementação de alto nível
que usa iterator.

Primeiro, criamos um Iterator que entra na coleção. A cada chamada do método next, o
Iterator retorna o próximo objeto do conjunto. Um iterator pode ser obtido com o método iterator()
de Collection, por exemplo, em uma lista de String:

Iterator<String> i = [Link]();

A interface Iterator tem dois métodos principais:

1. hasNext() (com retorno booleano), indica se ainda existe um elemento a ser percorrido; e

2. next() que retorna o próximo objeto.

Exemplo:

Set<String> conjunto = new HashSet<>();

[Link]("item 1");

[Link]("item 2");

[Link]("item 3");

// retorna o iterator

Iterator<String> i = [Link]();

while ([Link]()) {

// recebe a palavra

String palavra = [Link]();

[Link](palavra);

}
70

O Iterator pode, sim, ainda ser útil. Além de iterar na coleção, como faz o enhanced-for, o
Iterator consegue remover elementos da coleção durante a iteração de uma forma elegante por meio
do método remove.

11.2. [Link]

O primeiro dos recursos da API de Collections que listaremos é lista (List), que é uma
coleção a qual permite elementos duplicados e mantém uma ordenação específica entre os
elementos.

Em outras palavras, você tem a garantia de que, quando percorrer a lista, os elementos
serão encontrados em uma ordem pré-determinada, definida na hora das suas inserções. Ela resolve
todos os problemas os quais levantamos em relação ao array (busca, remoção, tamanho infinito,
etc.).

A API de Collections fornece a interface [Link] a qual especifica o que uma classe

deve ser capaz de fazer para ser uma lista. Há diversas implementações disponíveis, cada uma com
uma forma diferente de representar uma lista.

A implementação mais utilizada da interface List é a ArrayList, que trabalha com uma
array interna para gerar uma lista. Portanto, ela é mais rápida na pesquisa do que sua concorrente, a
LinkedList, a qual é mais rápida na inserção e remoção de itens nas pontas.

Uma lista fornece, além de acesso a um Iterator, um ListIterator, que oferece recursos
adicionais, específicos para listas. Usando o ListIterator, você pode, por exemplo, adicionar um
elemento à lista ou voltar ao elemento que foi iterado anteriormente. (Falta...)

11.2.1. ArrayList

IMPORTANTE: é comum confundirem um objeto da classe ArrayList (que implementa a


interface [Link]) com um array, porém ela não o é. O que ocorre é que, internamente, ela usa
uma array como estrutura para armazenar os dados, porém esse atributo está propriamente
encapsulado, e você não tem como acessá-lo. Repare também: você não pode usar [] com uma
ArrayList nem acessar o atributo length.

[Link]. Instanciar um ArrayList

Para criar um ArrayList, basta invocar o construtor:


71

ArrayList lista = new ArrayList();

É sempre possível abstrair a lista a partir da interface List:

List lista = new ArrayList();

Para criar uma lista de nomes (String), podemos fazer:

List lista = new ArrayList();

[Link](“Manoel”);

[Link](“Joaquim”);

[Link]("Maria");

A interface List tem dois métodos add, um que recebe o objeto a ser inserido e o coloca no
final da lista, e um segundo que permite adicionar o elemento em qualquer posição da lista. Note
que, em momento algum, dizemos qual é o tamanho da lista; podemos acrescentar quantos
elementos quisermos, pois a lista cresce conforme for necessário.

Toda lista (na verdade, toda Collection) trabalha do modo mais genérico possível. Isto é,
não há uma ArrayList específica para Strings, outra para números, outra para datas, etc. Todos os
métodos trabalham com Object.

Assim, é possível criar, por exemplo, uma lista de contas-correntes:

ContaCorrente c1 = new ContaCorrente();

[Link](100);

ContaCorrente c2 = new ContaCorrente();

[Link](200);

ContaCorrente c3 = new ContaCorrente();

[Link](300);

List contas = new ArrayList();

[Link](c1);

[Link](c3);

[Link](c2);

Para saber quantos elementos há na lista, usamos o método size():


72

[Link]([Link]());

Há ainda um método get(int), o qual recebe como argumento o índice do elemento que se
quer recuperar. Por meio dele, podemos executar um for para iterar na lista de contas:

for (int i = 0; i < [Link](); i++) {

[Link](i); // código não muito útil....

Mas como fazer para imprimir o saldo dessas contas? Podemos acessar o método
getSaldo() diretamente após fazer [Link](i)? Não podemos. Lembre-se de que toda lista trabalha
sempre com Object.

Assim, a referência devolvida pelo get(i) é do tipo Object, sendo necessário o cast para

ContaCorrente se quisermos acessar o getSaldo():

for (int i = 0; i < [Link](); i++) {

ContaCorrente cc = (ContaCorrente) [Link](i);

[Link]([Link]());

// Note que a ordem dos elementos não é alterada.

Como em qualquer lista é possível colocar qualquer Object, é possível misturar objetos.
Exemplo:

ContaCorrente cc = new ContaCorrente();

List lista = new ArrayList();

[Link]("Uma string");

[Link](cc);

E como recuperar esses objetos? Como o método get devolve um Object, precisamos fazer
o cast, mas, tendo uma lista com vários objetos de tipos diferentes, isso pode não ser tão simples.

Geralmente, não nos interessa uma lista com vários tipos de objetos misturados; no dia a
dia, usamos listas como aquela de contas-correntes. No Java 5.0, podemos usar o recurso de
Generics para restringir as listas a um determinado tipo de objetos (e não qualquer Object ).
exemplo:
73

List<ContaCorrente> contas = new ArrayList<ContaCorrente>();

[Link](c1);

[Link](c3);

[Link](c2);

Repare no uso de um parâmetro ao lado de List e ArrayList, pois ele indica que nossa lista
foi criada para trabalhar exclusivamente com objetos do tipo ContaCorrente. Isso nos traz uma
segurança em tempo de compilação, além do uso de Generics também eliminar a necessidade de
casting, uma vez que todos os objetos inseridos na lista serão, seguramente, do tipo ContaCorrente.
Exemplo:

for(int i = 0; i < [Link](); i++) {

ContaCorrente cc = [Link](i); // sem casting!

[Link]([Link]());

Por fim, a partir do Java 7, se você instancia um tipo genérico na mesma linha de sua
declaração, não é necessário passar os tipos novamente, basta usar new ArrayList<>(). É conhecido
como operador diamante. Exemplo: List<ContaCorrente> contas = new ArrayList<>().

Há ainda outros métodos, como por exemplo o remove(), o qual recebe um objeto que se
deseja remover da lista; e contains(), que recebe um objeto como argumento e devolve true ou false,
indicando se o elemento está ou não na lista.

Algumas listas, como a ArrayList , têm acesso aleatório aos seus elementos: a busca por
um elemento em uma determinada posição é feita de maneira imediata, sem que a lista inteira seja
74

percorrida (que chamamos de acesso sequencial). Nesse caso, o acesso por meio do método get(int)
é muito rápido.

Caso contrário, percorrer uma lista usando um for como esses que acabamos de ver pode
ser desastroso. Se for necessário percorrermos uma lista, devemos usar sempre um Iterator ou
enhanced for.

Ou seja: uma lista é uma excelente alternativa a uma array comum, já que temos todos os
benefícios de arrays sem a necessidade de tomar cuidado com remoções, falta de espaço, etc.

11.2.2. LinkedList

A outra implementação muito usada, a LinkedList, fornece métodos adicionais para obter e
remover o primeiro e último elemento da lista. Ela também tem o funcionamento interno diferente,
o que pode impactar performance.

(mais?)

11.2.3. Vector

A classe Vector implementa um array expansível de objetos. Como um array, ele contém
componentes que podem ser acessados usando um índice inteiro. No entanto, o tamanho de Vector
pode aumentar ou diminuir conforme necessário para acomodar a adição e remoção de itens após o
Vector ter sido criado. Para isto, ele gerencia esta parte da mesma forma que um ArrayList.

Os iteradores retornados pelos métodos da classe Vector são fail-fast: ou seja, se o vetor for
estruturalmente modificado a qualquer momento após o iterador ser criado o iterador lançará um
ConcurrentModificationException.

11.2.4. Método sort()

Vimos, anteriormente, que as listas são percorridas de maneira pré-determinada de acordo


com a inclusão dos itens. Mas, muitas vezes, queremos percorrer a nossa lista de maneira ordenada.

A classe Collections fornece um método estático sort , que recebe um List como argumento

e o ordena por ordem crescente. Por exemplo:

List<String> lista = new ArrayList<>();

[Link]("Sérgio");
75

[Link]("Paulo");

[Link]("Guilherme");

// Repare que o toString de ArrayList foi sobrescrito:

[Link](lista); //imprime na ordem de inserção.

[Link](lista);

[Link](lista); //imprime em ordem alfabética.

Ao testar o exemplo acima, você observará que, primeiro, a lista é impressa na ordem de
inserção e, depois de invocar o sort, ela é impressa em ordem alfabética.

Mas toda lista em Java pode ser de qualquer tipo de objeto, por exemplo, ContaCorrente. E
se quisermos ordenar uma lista de ContaCorrente? Em que ordem a classe Collections ordenará?
Pelo saldo? Pelo nome do correntista? Considere o exemplo abaixo:

ContaCorrente c1 = new ContaCorrente();

[Link](500);

ContaCorrente c2 = new ContaCorrente();

[Link](200);

ContaCorrente c3 = new ContaCorrente();

[Link](150);

List<ContaCorrente> contas = new ArrayList<>();

[Link](c1);

[Link](c3);

[Link](c2);

[Link](contas); // qual seria o critério para esta ordenação?

Sempre que falamos em ordenação, precisamos pensar em um critério de ordenação:s uma


forma de determinar qual elemento vem antes de qual.

É necessário instruir o método sort sobre como comparar nossas ContaCorrentes a fim de
determinar uma ordem na lista. Para isto, o método sort necessita que todos objetos da lista sejam
comparáveis e, portanto, tenham um método que compare um objeto ContaCorrente com outro
objeto ContaCorrente. Como que o método sort terá a garantia de que a sua classe tem esse método?
76

Isso será feito, novamente, por meio de um contrato, ou seja, de uma interface!

Façamos com que os elementos da nossa coleção implementem a interface


[Link] que define o método int compareTo(Object). Esse método deve retornar:

1. Zero, se o objeto comparado for igual àquele objeto;

2. Um número negativo, se aquele objeto for menor que o objeto dado; e

3. Um número positivo, se aquele objeto for maior que o objeto dado.

Assim, para ordenar os objetos da classe ContaCorrente por saldo, por exemplo, basta
implementar o Comparable. Exemplo:

public class ContaCorrente extends Conta implements Comparable<ContaCorrente> {

// Todo o código anterior fica aqui

public int compareTo(ContaCorrente outra) {

if ([Link] < [Link]) {

return -1;

} else if ([Link] > [Link]) {

return 1;

} esle

return 0;

Com o código anterior, nossa classe tornou-se “comparável”: dados dois objetos da classe,
conseguimos dizer se um objeto é maior, menor ou igual ao outro, segundo algum critério por nós
definido.

No nosso caso, a comparação será feita com base no saldo da conta. Repare que o critério
de ordenação é totalmente aberto, definido pelo programador. Se quisermos ordenar por outro
atributo (ou até por uma combinação de atributos), basta modificar a implementação do método
compareTo na classe.

Quando chamarmos o método sort de Collections, ele saberá como fazer a ordenação da
lista pois usará o critério que definimos no método compareTo.
77

Agora, vamos para outro exemplo. O que acha da implementação abaixo?

public int compareTo(Conta outra) {

return [Link]([Link](), [Link]());

Este caso é o mesmo do exemplo com uma lista de Strings. Por que a ordenação funcionou,
naquele caso, sem precisarmos fazer nada? Simples: quem escreveu a classe String (lembre-se de
que ela é uma classe como qualquer outra) implementou a interface Comparable e o método
compareTo para Strings, fazendo a comparação em ordem alfabética (consulte a documentação da
classe String e veja o método compareTo). O mesmo acontece com outras classes, como Integer
(exemplo acima), BigDecimal, Date, entre outras.

11.3. [Link]

Um conjunto (Set) funciona de forma análoga aos conjuntos da matemática: ele é uma
coleção que não permite elementos duplicados.

Outra característica fundamental desta interface é o fato de que a ordem na qual os


elementos são armazenados pode não ser aquela em que eles foram inseridos no conjunto. A
interface não define como deve ser esse comportamento. Tal ordem varia de implementação para
implementação.

Um conjunto é representado pela interface Set e tem como suas principais implementações
as classes HashSet , LinkedHashSet e TreeSet, conforme a presenta a figura abaixo.

O código abaixo cria um conjunto e adiciona diversos elementos, alguns repetidos:

Set<String> cargos = new HashSet<>();


78

[Link]("Gerente");

[Link]("Diretor");

[Link]("Presidente");

[Link]("Secretária");

[Link]("Funcionário");

[Link]("Diretor"); // repetido!

// imprime na tela todos os elementos

[Link](cargos);

No exemplo acima, o segundo “Diretor” não será adicionado, e o método add retornará
false. Ou seja: o método add de um Set retorna true se adicionou com sucesso, ou false caso
contrário.

O uso de um Set pode parecer desvantajoso, já que ele não armazena a ordem e não aceita
elementos repetidos. Não há métodos que trabalham com índices, como o get(int) que as listas têm.

A grande vantagem do Set é a existência de implementações como a HashSet que têm uma
performance incomparável com as Lists quando usadas para pesquisa (método contains, por
exemplo).

Teste (falta alguma coisa?)

11.3.1. HashSet

Esta classe implementa a interface Set, apoiada por uma tabela hash, na verdade, uma
instância HashMap. Ela não garante a ordem de iteração do conjunto; em particular, ela não garante
que a ordem permanecerá constante ao longo do tempo. Esta classe permite o elemento nulo.

Note que esta implementação não é sincronizada. Se vários threads acessarem um conjunto
de hash simultaneamente e, pelo menos um dos threads modificar o conjunto, pode gerar erro de
busca, por exemplo. Para evitar isto, a criação do um objeto da classe HashSet deve ser
sincronizada no momento da criação,“encapsulando” usando o método estático
[Link]. Isso é melhor feito no momento da criação, para evitar acesso
acidental não sincronizado ao conjunto. Exemplo: Set s = [Link](novo
HashSet(…)).

Os iteradores retornados pelo método iterator desta classe também são fail-fast.
79

11.3.2. TreeSet

Esta implementação fornece custo de tempo log(n) garantido para as operações básicas
(add, remove e contains), e os elementos são ordenados usando sua ordenação natural, ou por um
objeto Comparator fornecido no momento da criação do conjunto, dependendo de qual construtor é
usado.

Note que a ordenação mantida por um conjunto (seja ou não fornecido um comparador
explícito) deve ser consistente com equals se for para implementar corretamente a interface Set.
Isso ocorre porque a interface Set é definida em termos da operação equals, mas uma instância
TreeSet realiza todas as comparações de elementos usando seu método compareTo (ou compare),
então, dois elementos que são considerados iguais por esse método são, do ponto de vista do
conjunto, iguais.

Note que esta implementação não é sincronizada como as anteriores, portanto, também
precisa usar um método estático de sincronização: SortedSet s =
Coleçõ[Link](novo TreeSet(…)).

Os iteradores retornados pelo método iterator desta classe também são fail-fast.

11.3.3. LinkedHashSet

Esta classe é uma implementação de Hashtable e lda ista encadeada da interface Set, com
ordem de iteração previsível. Esta implementação difere de HashSet porque mantém uma lista
duplamente encadeada em execução em todas as suas entradas. Esta lista encadeada define a ordem
de iteração, que é a ordem em que os elementos foram inseridos no conjunto. Observe que a ordem
de inserção não é afetada se um elemento for reinserido no conjunto.

Esta implementação poupa seus clientes da ordenação não especificada e, geralmente,


caótica fornecida por HashSet, sem incorrer no custo aumentado associado a TreeSet.

Esta classe fornece todas as operações Set opcionais e permite elementos nulos e os
iteradores retornados pelo método iterator desta classe são fail-fast.

Note que esta implementação não é sincronizada como as anteriores, portanto, também
precisa usar um método estático de sincronização: Set s = [Link](new
LinkedHashSet(...)).
80

12. [Link]
Muitas vezes, queremos buscar rapidamente um objeto a partir de alguma informação
sobre ele. Um exemplo seria obter todos os dados do carro a partir de sua placa. Poderíamos utilizar
uma lista para isso e percorrer todos os seus elementos, mas pode ser péssimo para a performance
até para listas não muito grandes. Aqui entra o mapa, conforme a presenta a figura abaixo.

Um mapa é composto por um conjunto de associações entre um objeto-chave e um objeto-


valor. É equivalente ao conceito de dicionário, usado em várias linguagens. Algumas linguagens,
como Perl ou PHP têm um suporte mais direto a mapas, também chamados de matrizes/arrays
associativas.

“[Link]” é um mapa, pois é possível usá-lo para mapear uma chave a um valor, por
exemplo: mapeie o valor “caelum” à chave “empresa”, ou então, o valor “Vergueiro” à chave “rua”.
Semelhante a associações de palavras que podemos fazer em um dicionário.

Observe o exemplo: criamos duas contas-correntes e as colocamos em um mapa,


associando-as aos seus donos.

ContaCorrente c1 = new ContaCorrente();

[Link](10000);

ContaCorrente c2 = new ContaCorrente();

[Link](3000);

// cria o mapa

Map<String, ContaCorrente> mapaDeContas = new HashMap<>();

// adiciona duas chaves e seus respectivos valores

[Link]("diretor", c1);
81

[Link]("gerente", c2);

// qual a conta do diretor? (sem casting!)

ContaCorrente contaDoDiretor = [Link]("diretor");

[Link]([Link]());

Um mapa é muito usado para indexar objetos de acordo com determinado critério com o
intuito de buscá-los rapidamente.

Ele, assim como as coleções, trabalha diretamente com Objects (tanto na chave quanto no
valor), o que tornaria necessário o casting no momento em que recuperar elementos. Usando os
generics, como fizemos aqui, não precisamos mais do casting.

As operações básicas do Map são:

1. “put(Object key, Object value)”: é usado para inserir um mapeamento em um mapa, mas, e
uma chave existente for passada, o valor anterior será substituído pelo novo valor. Se um
novo par for passado, o par será inserido como um todo. Se uma chave existente for passada,
então o valor anterior será retornado. Se um novo par for passado, então NULL será
retornado;

2. “get(Objetc key)”: é usado para recuperar ou buscar o valor mapeado por uma chave
específica mencionada no parâmetro. O método retorna o valor associado ao parâmetro key,
mas retorna NULL quando o mapa não contém tal mapeamento para a chave;

3. “containsKey(Object key)”: é usado para verificar se uma chave específica está sendo
mapeada no HashMap ou não. Ele obtém o elemento key como um parâmetro e retorna true
se esse elemento estiver mapeado no mapa, caso contrário, retorna false;

4. “containsValue(Object value)”: é usado para verificar se um valor específico está sendo


mapeado por uma única ou mais chaves no HashMap. Ele obtém o value como um
parâmetro e retorna true se esse valor for mapeado por qualquer uma das chaves no mapa,
caso contrário, retorna false;

5. “size()”: é usado para obter o tamanho do mapa, que se refere ao número de pares chave-
valor ou mapeamentos no Mapa. O método não aceita nenhum parâmetro.; e

6. isEmpty(): é usado para verificar se o mapa está vazio. O método retorna true se não houver
nenhum par chave-valor estiver presente no mapa, senão False. O método não aceita
nenhum parâmetro..
82

Fonte: [Link] e [Link]

Apesar de o mapa fazer parte do framework, ele não estende a interface Collection por ter
um comportamento bem diferente, mas alguns de seus métodos retornam coleções internas para um
objeto Map. Veja o diagrama da interface Map.

Por exemplo: o método keySet() retorna um Set com as chaves daquele mapa, e o método
values() retorna a Collection com todos os valores que foram associados a alguma das chaves.

Suas principais implementações são o HashMap, o TreeMap e o Hashtable, conforme


apresenta a figura abaixo.

Fonte: [Link]

Com relação a complexidade computacional, a figura abaixo apresenta cada uma das
implementações da interface Map.
83

Fonte: [Link] e autor.

12.1. HashMap

É uma implementação baseada em tabela hash da interface Map, que fornece todas as
operações de mapa opcionais e permite valores null tanto na chave quanto nos valores.

A classe HashMap é, aproximadamente, equivalente a Hashtable, exceto que não é


sincronizada e permite nulos. Esta classe não dá garantias quanto à ordem do mapa; em particular,
não garante que a ordem permanecerá constante ao longo do tempo.
Fonte: [Link] e autor.

Uma instância de HashMap tem dois parâmetros que afetam seu desempenho: capacidade
inicial e fator de carga, conforme estudares no subcapítulo Erro: Origem da referência não
encontrada.

Se muitos mapeamentos forem armazenados em uma instância de HashMap, criá-la com


uma capacidade suficientemente grande permitirá que os mapeamentos sejam armazenados de
forma mais eficiente do que deixá-la executar rehashing automático, conforme necessário para
aumentar a tabela. Observe que usar muitas chaves com o mesmo hashCode() é uma maneira segura
de diminuir o desempenho de qualquer tabela de hash. Para melhorar o impacto, quando as chaves
são Comparable, esta classe pode usar a ordem de comparação entre as chaves para ajudar a
desempatar.
Fonte: [Link] e autor.

Observe que esta implementação não é sincronizada. Se vários threads acessarem um


hashmap simultaneamente, e pelo menos um dos threads modificar o mapa estruturalmente, ele
deve ser sincronizado externamente (uma modificação estrutural é qualquer operação que adiciona
ou exclui um ou mais mapeamentos; apenas alterar o valor associado a uma chave que uma
instância já contém não é uma modificação estrutural). Isso é, normalmente, realizado pela
sincronização em algum objeto que, naturalmente, encapsula o mapa. Se nenhum objeto desse tipo
existir, o mapa deve ser "encapsulado" usando o método [Link]. Isso é
84

melhor feito no momento da criação, para evitar acesso acidental não sincronizado ao mapa.
Exemplo: Map m = [Link](new HashMap(…)).
Fonte: [Link] e autor.

Os iteradores retornados por todos os “métodos de visualização de coleção” desta classe


são fail-fast (falha-rápida): se o mapa for estruturalmente modificado a qualquer momento após o
iterador ser criado, de qualquer forma, exceto por meio do próprio método remove do iterador, o
mesmo lançará um ConcurrentModificationException. Assim, diante de modificação simultânea, o
iterador falha rápida e claramente, em vez de arriscar comportamento arbitrário e não determinístico
em um momento indeterminado no futuro.
Fonte: [Link] e autor.

12.2. TreeMap

O treemaps exibe dados hierárquicos. TreeMap implementa a interface NavigableMap, que


herda SortedMap que, por sua vez, herda a interface Map.
Fonte: [Link]

Esta implementação fornece custo de tempo garantido de log(n) para as operações


containsKey() get(), put() e remove().
Fonte: [Link] e autor.

Ao implementar as interfaces NavigableMap e SortedMap, o TreeMap recebe


funcionalidade adicional que não está disponível no HashMap, mas paga um preço em termos de
desempenho.
Fonte: [Link]

Embora a classe TreeMap seja a mais versátil, ela nem sempre pode armazenar null como
uma chave. Além disso, acessar os elementos de um TreeMap leva mais tempo. Portanto, se você
não precisa armazenar dados em alguma ordem de classificação, é melhor usar HashMap ou
LinkedHashMap.
Fonte: [Link]

12.2.1. Árvore “red-balck”

Você, provavelmente, notou que o TreeMap usa uma estrutura de dados chamada árvore
rubro-negra. Armazenar dados nesta estrutura é precisamente o que fornece ordenação de dados.
Então, que tipo de árvore é essa? Imagine que você precise armazenar pares Number-String. Os
85

números 16, 20, 52, 55, 61, 65, 71, 76, 81, 85, 90, 93 e 101 serão chaves. Se você armazenar dados
em uma lista tradicional e precisar encontrar o elemento com a chave 101, precisará percorrer todos
os 13 elementos para encontrá-lo., o que não é grande coisa, mas ao trabalhar com um milhão,
teremos grandes problemas. Para resolver isto, os programadores usam estruturas de dados um
pouco mais complexas. É aqui que a árvore rubro-negra entra em cena. Verifique o exemplo
apresentado na figura abaixo.

Fonte: [Link]

A busca por um elemento começa na raiz da árvore que no nosso exemplo, é o número 61.
Então, comparamos os valores dos nós com o valor que estamos procurando. Se nosso valor for
menor, vamos para a esquerda; se for maior, então vamos para a direita. Esse processo se repete até
encontrarmos o valor desejado ou encontrarmos um elemento cujo valor seja nulo (uma folha da
árvore).
Fonte: [Link] e autor.

12.2.2. Métodos de SortedMap e NavigableMap

Assim como o HashMap, o TreeMap implementa a interface Map, o que significa que ele
possui todos os métodos existentes no HashMap, mas também implementa as interfaces SortedMap
e NavigableMap e, portanto, obtém funcionalidades adicionais delas.

SortedMap é uma interface que estende Map e adiciona métodos relevantes a um conjunto
de dados classificado.
Fonte: [Link] e autor.

Já NavigableMap é uma interface que estende SortedMap e adiciona métodos para navegar
entre os elementos de um mapa.
Fonte: [Link]
86

12.3. Hashtable

Uma hashtable é muito parecida com um map, pois implementa a interface Map com a
diferença que não aceita null para chaves e valores.
Fonte: [Link]

Em ciência da computação, uma tabela de dispersão (também conhecida por tabela de


espalhamento ou “Hashtable”, em inglês) é uma estrutura de dados especial que associa chaves de
pesquisa a valores. Seu objetivo é, a partir de uma chave simples, fazer uma busca rápida e obter o
valor desejado. Algumas vezes, é traduzida como tabela de escrutínio.
Fonte: [Link] e autor.

Os registros armazenados em uma tabela são endereçados a partir de uma transformação


aritmética sobre a chave de pesquisa cujo resultado é um código hash, ou “hashcode”.
Fonte: [Link]

Hashtable é uma estrutura de dados onde os dados são armazenados em um formato de


array. Cada valor de dados tem um valor de chave exclusivo. Se a chave for conhecida, o acesso aos
dados necessários é muito rápido. Assim, as operações de inserção e pesquisa são rápidas
independentemente do tamanho dos dados. Hashtable consiste em um array para manter os dados e
um método de hashing para geração de um índice onde um elemento deve estar localizado. O que é
hash? É uma regra que mapeia o Objeto em um conjunto de caracteres (código). Normalmente, esse
tipo de função converte um grande pedaço de dados em um pequeno valor inteiro. As funções de
hash podem ser diferentes, mas todas apresentam certas propriedades:
Fonte: [Link]

1. Um objeto específico tem o “hashcode” específico;

2. Dois objetos iguais têm os mesmos “hashcodes”. O contrário não é verdade;

3. Se dois “hashcodes” forem diferentes, os objetos, definitivamente, não são iguais; e

4. Objetos diferentes podem ter o mesmo “hashcode”. Este evento, muito raro, se chama
“colisão”. Um bom método de hashing minimiza a probabilidade de colisões.

Tabelas de dispersão são tipicamente utilizadas para implementar vetores associativos,


conjuntos e caches. São tipicamente usadas para indexação de grandes volumes de informação
(como bases de dados). A implementação típica busca uma função de dispersão que seja de
complexidade3 O(1), não importando o número de registros na tabela (desconsiderando colisões). O

3 A teoria da complexidade computacional é um ramo da teoria da computação em ciência da computação teórica e


87

ganho real com relação a outras estruturas associativas (como um vetor simples) passa a ser maior
conforme a quantidade de dados aumenta.
Fonte: [Link] e autor.

Ou seja: objetivo é ter eficiência linear de O(1) nas operações de busca, inserção e
remoção. Para isso, as inserções e remoções não devem provocar grandes variações na quantidade
de registros armazenados.

A capacidade é o número de buckets na tabela hash, e a capacidade inicial diz respeito a


capacidade no momento em que a tabela hash é criada.

O fator de carga é uma medida de quão cheia a tabela hash pode ficar antes que sua
capacidade seja aumentada automaticamente. Quando o número de entradas na tabela hash excede o
produto do fator de carga e a capacidade atual, a tabela hash é refeita (ou seja, as estruturas de dados
internas são reconstruídas) para que a tabela hash tenha aproximadamente o dobro do número de
buckets.
Fonte: [Link] e autor.

Como regra, o fator de carga padrão (.75) oferece um bom tradeoff (troca) entre custos de
tempo e espaço. Valores mais altos diminuem a sobrecarga de espaço, mas aumentam o custo de
pesquisa (refletido na maioria das operações da classe HashMap, incluindo get() e put()). O número
esperado de entradas no mapa e seu fator de carga devem ser levados em consideração ao definir
sua capacidade inicial, de modo a minimizar o número de operações de “re-hashing”. Se a
capacidade inicial for maior que o número máximo de entradas dividido pelo fator de carga,
nenhuma operação de re-hashing ocorrerá.
Fonte: [Link] e autor.

Para armazenar e recuperar objetos com sucesso de uma tabela de hash, os objetos usados
como chaves devem implementar os métodos hashCode e equals, mas, antes, é importante entender
a “função de espalhamento”, ou “hashing”.
Fonte: [Link]

12.3.1. Hashing

“Hashing” ou função de espalhamento, ou função de dispersão é a responsável por gerar


matemática que se concentra em classificar problemas computacionais de acordo com sua dificuldade inerente, e
relacionar essas classes entre si. Neste contexto, um problema computacional é entendido como uma tarefa que é,
em princípio, passível de ser resolvida por um computador (o que basicamente significa que o problema pode ser
descrito por um conjunto de instruções matemáticas). Fonte:
[Link]
88

um índice a partir de determinada chave. Caso a função seja mal escolhida, toda a tabela terá um
mau desempenho.

Uma função “hashing” gera novos valores de acordo com um algoritmo de “hashing”
matemático, conhecido como “valor hash” ou simplesmente “hash”. Para evitar a conversão de um
hash de volta para a chave original, um bom hash sempre usa um algoritmo de hash unidirecional.
Fonte: [Link]

Um processo “hash” possui três componentes:

Entrada: os dados inseridos no algoritmo são chamados de entrada. Esses dados podem ter
qualquer tamanho e formato. Por exemplo, uma entrada pode ser um arquivo de música ou um
artigo. No processo “hash”, cada pedaço de dados de entrada é usado para produzir uma única
saída;

Função de hash: a parte central do processo de hash é a função hash que obtém os dados de
entrada e aplica uma série de operações matemáticas a eles, resultando em uma sequência de
caracteres de comprimento fixo. A função hash garante que mesmo uma pequena alteração nos
dados de entrada produza um valor hash significativamente diferente; e

Saída de hash: diferentemente da entrada, a saída do processo de hash ou valor de hash (o


hashCode) tem um comprimento definido. É desafiador determinar o comprimento da entrada
original porque as saídas têm um comprimento definido, o que contribui para um aumento geral na
segurança. Um hashcode é uma sequência de caracteres e números que um hacker pode não
conseguir ler, mantendo as informações privadas de uma pessoa. Como cada hashcode é distinto,
eles também são, frequentemente, chamados de impressões digitais.

O ideal para a função de espalhamento é que sejam sempre fornecidos índices únicos para
as chaves de entrada. A função perfeita seria a que, para quaisquer entradas A e B, sendo A diferente
de B, fornecesse saídas diferentes. Quando as entradas A e B são diferentes e, passando pela função
de espalhamento, geram a mesma saída, acontece o que chamamos de colisão. Analise a figura
abaixo
Fonte: [Link]
89

Figura 17: tabela de dispersão com vetor simples e com vetor de


listas.

Fonte: [Link]

Devido às colisões, muitas tabelas de dispersão são aliadas com alguma outra estrutura de
dados, tal como uma lista encadeada ou até mesmo com árvores balanceadas. Em outras
oportunidades a colisão é solucionada dentro da própria tabela.

[Link]. Exemplo

Imagine que seja necessário utilizar uma tabela de dispersão para otimizarmos uma busca
de nomes de uma lista telefônica onde, informado o nome, temos que obter o endereço e o telefone.
Nesse caso, poderíamos armazenar toda a lista telefônica em um vetor e criar uma função de
espalhamento que funcionasse de acordo com o seguinte critério: para cada nome começado com a
letra A, devolver 0. Para cada nome começado com a letra B, devolver 1, e assim por diante. Por
fim, Para cada nome começado com a letra Z, devolver 25 (Alfabeto com 26 letras). Este é o
procedimento do “hashing”.
Fonte: [Link] e autor.

Agora inserimos alguns nomes em nossa lista telefônica:

1. José da Silva; Rua das Almas, 35; Telefone (31) 3888-9999;

2. Ricardo Souza; Rua dos Coqueiros, 54; Telefone (31) 3222-4444; e

3. Orlando Nogueira; Rua das Oliveiras, 125; Telefone (31) 3444-5555.

Nossa função distribuiria os nomes assim:


Fonte: [Link] e autor.
90

Figura 18: tabela de hash sem colisões.

Fonte: [Link]

Agora inserimos mais um nome:

1. Renato Porto; Rua dos Elefantes, 687; Telefone (31) 3333-5555.

E temos uma colisão, conforme apresenta a figura 3:

Figura 19: tabela de hash com colisões.

Fonte: [Link]

Verifica-se que, com a função “hashing” do exemplo, a colisão ocorre assim que uma nova
entrada na lista telefônica, com um nome iniciando-se pela letra R, for adicionada. Tem-se, então,
uma colisão com a letra R. Outro exemplo: se for adicionado "João Siqueira", a entrada estaria em
conflito com o "José da Silva".

12.4. Métodos equals e hashcode

Assim, muitas das coleções do Java guardam os objetos dentro de tabelas de hash. Essas
tabelas são utilizadas para que a pesquisa de um objeto seja feita de maneira rápida.

Como funciona? Cada objeto é "classificado" pelo seu hashCode e, com isso, conseguimos
espalhar cada objeto, agrupando-os pelo hashCode . Quando buscamos determinado objeto, só
procuraremos entre os elementos que estão no grupo daquele hashCode . Dentro desse grupo,
testaremos o objeto procurado com o candidato sobrescrevendo (override) o método equals() .

A fim de que isso funcione direito, o método hashCode de cada objeto deve retornar o
mesmo valor aos dois objetos se eles são considerados equals. Em outras palavras: [Link](b)
implica [Link]() == [Link]().
91

“Overrride” hashCode de tal maneira que ele retorne valores diferentes a dois objetos
considerados equals por seus hashCodes quebra o contrato de Object, e isso resultará em collections
que usam espalhamento (como HashSet, HashMap e Hashtable) não encontrando objetos iguais
dentro de uma mesma coleção.

12.5. Properties

Um mapa importante é a tradicional classe Properties, que herda Hashtable, mapeando


Strings e é muito utilizada para a configuração de aplicações.

A classe Properties tem também métodos para ler e gravar o mapeamento com base em um
arquivo-texto, facilitando muito a sua persistência. Exemplo:

Properties config = new Properties();

[Link]("[Link]", "scott");

[Link]("[Link]", "tiger");

[Link]("[Link]","jdbc:mysql:/localhost/teste");

// muitas linhas depois…

String login = [Link]("[Link]");

String password = [Link]("[Link]");

String url = [Link]("[Link]");

[Link](url, login, password);

Repare que não houve a necessidade do casting para String no momento de recuperar os
objetos associados. Isso porque a classe Properties foi desenhada a fim de trabalhar com a
associação entre Strings.

12.6. Boas práticas

As coleções do Java oferecem grande flexibilidade ao usuário. A perda de performance em


relação à utilização de arrays é irrelevante, mas deve-se tomar algumas precauções:

1. Grande parte das coleções usam, internamente, uma array para armazenar os seus dados.
Quando essa array não é mais suficiente, é criada uma maior, e o conteúdo da antiga é
copiado. Este processo pode acontecer muitas vezes caso tenha uma coleção que cresce
muito. Você deve, então, criar uma coleção já com uma capacidade grande a fim de evitar o
92

excesso de redimensionamento;

2. Evite usar coleções que guardam os elementos pela sua ordem de comparação quando não
há necessidade. Um TreeSet, como estudado, gasta, computacionalmente, O(log(n)) para
inserir, pois ele utiliza uma árvore rubro-negra como implementação, enquanto o HashSet
gasta apenas O(1); e

3. Não itere sobre uma List utilizando um for de 0 até [Link]() e usando get(int) para receber
os objetos. Enquanto isso parece atraente, algumas implementações da List não são de
acesso aleatório como a LinkedList, o que faz esse código ter uma péssima performance
computacional. Use Iterator.

13. Interface Comparator


Para entender melhor as duas interfaces Comparator e Comparable, vamos estudar o
exemplo na figura abaixo, em que gostaríamos de ordenar uma lista de Contas bancárias. Cada
conta possui um número (int) e um titular (String)

O método para ordenar uma lista se encontra na classe [Link] (repare o ‘s’ no
final). Ela possui métodos estáticos que ajudam a manipular coleções, entre eles o método sort.
Assim podemos tentar ordenar a lista de contas: [Link](lista).

Mas, infelizmente, a linha acima nem compila. Antes de invocar o método sort, é preciso
definir o critério de ordenação: uma forma de informar, dado duas contas, qual vem “antes” e qual
vem “depois”.

Considerando que queremos ordenar pelo número da conta, a classe Conta pode
implementar a interface [Link] que define o que será nossa “ordem natural”. A
interface possui apenas um método compareTo:
93

As contas, então, devem ser comparáveis. Vamos definir a ordem natural baseada no
número da conta:

Se o número da conta atual é menor do que da outraConta retormamos -1 (ou qualquer int
negativo, indicando que this deve vir “antes” de outraConta), se for maior retornamos 1 (ou
qualquer int positivo) e se for igual então devolvemos 0. Agora podemos invocar
[Link](lista).

E se precisarmos ordenar uma lista com outro critério de comparação? Se precisarmos


alterar a própria classe e mudar seu método compareTo, teremos apenas uma forma de comparação
por vez. Precisamos de mais!

É possível definir outros critérios de ordenação usando a interface do pacote [Link]


chamada Comparator. Existe um outro método sort (overloading) em Collections que recebe, além
da List, um objeto de uma classe que implementou a interface Comparator definindo um critério de
ordenação específico. É possível ter vários Comparators com critérios diferentes para usar quando
for necessário.

E se surgir a necessidade de ordenar pelo titular da conta? Não queremos alterar o método
compareTo na classe Conta, já que isso mudaria a ordem natural. Queremos definir um outro
critério de ordenação. Para tal, existe uma outra interface: a Comparator
94

Como exemplo, criaremos um Comparator que serve para ordenar Strings de acordo com
seu tamanho.

Vamos, então, implementar a interface para definir a ordem pelo titular (String) da conta.
Comparar duas Strings é difícil, mas, como você pode imaginar, esse problema já foi resolvido na
API do Java.

A classe String já sabe comparar dois strings, sabemos disso, pois ela implementa a
interface Comparable (por esse mesmo motivo, podemos invocar [Link] para uma List de
String). Podemos, então, delegar essa tarefa ao método compareTo das Strings:

Como escolher para que este critério de comparação seja utilizado em vez da ordem
natural? O método sort é sobrecarregado e pode receber um objeto do tipo Comparator:

Falta mencionar que o método compareTo da interface Comparable deve ser consistente
com o método equals. Quando uma conta é igual a outra (a classe Conta sobreescreve – overringing
– o método equals para definir igualdade), o método compareTo deve devolver zero também.
Devemos também pensar se receberemos null como Contas para ordenar, e tomar as devidas
precauções nos comparadores.

Através de Comparable e Comparator também são controladas a ordenação da coleção


TreeSet e as chaves do mapa TreeMap.

Outro exemplo é utilizar apenas o metodo “compare” da interface Comparator. Criaremos


um Comparator que serve para ordenar Strings de acordo com seu tamanho, conforme apresenta a
figura abaixo.
95

Podemos deixá-lo mais curto, aproveitando o método estático auxiliar “[Link]”,


que compara dois inteiros:

Depois, dentro do nosso código, teríamos que chamar a [Link], passando o


comparador também:

Como a variável temporária comparador é utilizada apenas aí, é comum escrevermos


diretamente [Link](lista, new ComparadorPorTamanho()).

14. Classes aninhadas


Aninhadas são classes que estão dentro de outra classe, ou do inglês “Nested Classes”.
Uma classe pode ter uma ou várias classes aninhadas, e uma classe aninhada pode ter uma ou várias
outras classes aninhadas e assim por diante.

Em Java existem 4 tipos de classes aninhadas:

1. Classes internas: são classes não estáticas dentro de uma classe externa;

2. Classes estáticas: são classes estáticas dentro de uma classe externa;

3. Classes locais: são classes dentro de métodos; e


96

4. Classes anônimas: são classes criadas dinamicamente.

14.1.1. Não estática

Também chamadas de internas, para existir, precisa que um objeto da classe externa seja
instanciado. Tem acesso aos atributos e métodos da classe externa.

14.1.2. Estática

É uma classe que não pertence ao objeto de uma classe, mas sim à própria classe. Por isto,
não tem acesso aos métodos e atributos da classe externa que não sejam estáticos.

14.1.3. Anônimas

Classes anônimas em Java são classes criadas dinamicamente.

Repare que a classe ComparadorPorTamanho do capítulo anterior é bem pequena. É


comum haver a necessidade de criar vários critérios de comparação e, muitas vezes, eles são
utilizados apenas em um único ponto do nosso programa.

Há uma forma de escrever essa classe e instanciá-la em uma única instrução, sem a
necessidade de um identificador. Você faz isso invocando “new” em Comparator. Porém, se
dissemos que uma interface não pode ser instanciada, como isto é permitido? Realmente “new
Comparator()” não compila, mas compilará se você abrir chaves e implementar tudo o que é
necessário, equivalendo a uma implementação como em uma classe comum. Veja o código:

Em uma única linha, nós definimos uma classe e a instanciamos! Uma classe que nem
mesmo nome tem. Por esse motivo, o recurso é chamado de classe anônima. Ele aparece com certa
frequência, em especial, para não precisar implementar interfaces em que o código dos métodos
seria muito curto e não reutilizável.

Há ainda como diminuir mais o código, evitando a criação da variável temporária


97

comparador e instanciando a interface dentro da invocação para o sort:

15. Funções Lambda


Vamos apresentar as funções (ou expressões) lambda, primeiro, refazendo o exemplo da
figura anterior, onde, em vez de usarmos uma classe anônima sendo passada como parâmetro do
método estático “sort” de “Colletions”, vamos usar o equivalente em lambda:

[Link](lista, (s1, s2) -> [Link]([Link](), [Link]()));

O código compacto, (s1, s2) -> [Link]([Link](), [Link]()), passado como


argumento do método estático “sort” gerará uma instância da interface Comparator em que o
método compare retorna [Link]([Link], [Link]). Até mesmo o return não é
necessário, já que só temos uma instrução após o símbolo “->”. Esse é o recurso de lambda do Java
8.

Simplificando um pouco a definição, uma função lambda é uma função sem declaração,
isto é, não é necessário colocar um nome, um tipo de retorno e o modificador de acesso. A ideia é
que o método seja declarado no mesmo lugar em que será usado. As funções lambda em Java tem a
sintaxe definida como (argumento) -> (corpo), como mostram alguns exemplos da figura abaixo.

Uma função lambda pode ter nenhum ou vários parâmetros, e seus tipos podem ser
declarados ou podem ser omitidos, desta forma, eles são inferidos pelo Java (tipagem fraca?).
98

A função lambda pode ter nenhum ou vários comandos:

1. Se a mesma tiver apenas um comando, as chaves não são obrigatórias e a função retorna o
valor calculado na expressão; e

2. Se a função tiver vários comandos, é necessário colocar as chaves e também o comando


“return”; se nada for retornado, a função tem um retorno void.

Para mais demonstrações de como utilizar as funções lambada em Java, vamos analisar
alguns casos.

15.1. Threads

O primeiro exemplo será com threads, onde elas são muito utilizadas para simplificar o
código, analisando um programa que cria uma thread com uma função interna e que vai apenas
mostrar a mensagem “Thread com classe interna!”. O código desta implementação é apresentado na
figura abaixo:

Primeiro, é criada uma implementação do método run da interface Runnable e, em seguida,


é criada a Thread com essa implementação. É possível verificar a grande quantidade de código
necessário para um exemplo bastante simples.

Já com a utilização de expressões lambda, o código necessário para a implementação dessa


mesma funcionalidade é bastante simples e bem menor que o anterior, conforme apresenta a figura
abaixo:

Essa expressão não passa nenhum parâmetro, pois ela será passada para a função run,
definida na interface Runnable, que não tem nenhum parâmetro, então, ela também não tem
nenhum retorno.

Um código ainda mais simples é a passagem da função diretamente como parâmetro para o
construtor da classe Thread. A próxima figura apresenta um exemplo desse código, em que as
funções lambda podem ser definidas e passadas como parâmetros diretamente para outros métodos,
99

e isso pode ser bastante útil.

15.2. Colletions

As funções lambdas podem ser bastante utilizadas com as classes de coleções do Java, pois
são tulizadas diversos tipos de funções que consistem, basicamente, em percorrer a coleção e fazer
uma determinada ação, como, por exemplo, imprimir todos os elementos da coleção, filtrar
elementos da lista e buscar um determinado valor na lista. Vamos analisar a figura abaixo:

Com as funções lambda é possível implementar a mesma funcionalidade com muito menos
código, bastando chamar o método “forEach” de uma lista, que é um método que espera uma
função lambda como parâmetro. Esse método executará, a cada iteração na lista, a função passada
como parâmetro, conforme apresenta a figura abaixo.

Dentro do código de uma função lambda, ainda é possível executar diversos comandos,
como, por exemplo, antes de imprimir o número, verificar se ele é par ou ímpar: se for par o número
é impresso, caso contrário, nada é realizado. Nesse exemplo é possível verificar que, dentro de uma
expressão lambda pode, ser realizado qualquer tipo de operação, conforme apresenta a figura
abaixo.
100

Mais um exemplo do que pode ser feito com funções lambda, são as expressões
matemáticas. Na figura abaixo, é exibido o código para mostrar o quadrado de todos os elementos
de uma lista de números inteiros.

Como já estudamos, as funções lambda podem ser utilizadas também para a ordenação de
listas com a interface Comparator. Outro exemplo seria para o caso em que exista uma classe
Pessoa com os atributos nome e idade e seja necessário ordenar uma lista em ordem alfabética pelo
nome, ou em ordem das idades, então, é necessário implementar dois comparators, um para cada
tipo de parâmetro, e chamá-lo no método sort da lista que será ordenada. A próxima figura apresenta
o código do exemplo descrito utilizando a interface Comparator tradicional. O código da classe
Pessoa foi omitido, mas ela é uma classe bastante simples, apenas com os atributos nome e idade, o
construtor e os métodos get e set.

É fácil observar que o código, apesar de bastante simples, ficou muito grande apenas para
ordenar duas vezes a lista com dois parâmetros diferentes.
101

É possível reimplementar esse exemplo utilizando funções lambda e deixando o código


muito mais conciso. A próxima figura apresenta a reimplementarão da figura anterior, mas, agora,
utilizando expressões lambda.

Vejam que os dois exemplos implementam exatamente a mesma funcionalidade, mas o


código utilizando expressões lambda tem apenas cinco linhas, enquanto que o código que não
utiliza expressões lambda tem 15 linhas.

Com expressões lambda também é possível filtrar elementos de uma coleção de objetos
criando, para isso, um stream de dados (“fluxo”, também um novo conceito do Java 8), em que é
chamado o método filter do stream e, como parâmetro para esse método, é passado uma função
lambda que faz o filtro dos elementos desejados. A próxima figura apresenta dois exemplos de
filtros sendo realizados também na listagem de pessoas utilizadas nos exemplos anteriores. O
primeiro filtro é feito apenas para pessoas com mais de 30 anos, e o segundo para apenas pessoas
que tem o nome iniciado com a letra “E”.

15.3. Listeners

Listeners são classes que implementam o padrão de projeto Observer, que representa
objetos que ficam esperando ações realizadas em outros objetos e, a partir dessa ação, executam
algum código. Um exemplo bem comum são os Listeners de botões da API de interfaces gráficas
Swing. Por exemplo, quando é necessário implementar um código para realizar alguma ação
102

quando um usuário clica em um JButton, passamos um objeto do tipo ActionListener para o método
addActionListener do botão e isso, normalmente, é implementado com classes internas. A figura a
seguir apresenta o código para a criação desse listener com uma classe anônima.

Apesar de funcionar, o código exibido é muito grande, mas utilizando funções lambda é
possível implementar a mesma funcionalidade com um código muito mais enxuto e simples de
entender, conforme apresenta a próxima figura com este mesmo código, mas utilizando funções
lambda.
103

15.4. Lambda como argumentos

Além de escrever funções lambda, também é possível criar métodos que as recebam como
parâmetro, o que é bastante útil e pode tornar um método bastante flexível. Por exemplo, podemos
criar um método genérico para imprimir elementos de uma lista, mas passamos como parâmetro a
função lambda para a filtragem dos elementos dessa lista.

Assim, com apenas um método e passando a função como parâmetro, é possível fazer a
filtragem da lista de várias maneiras diferentes. A próxima figura apresenta um exemplo de como
poderia ser feito isso.
104

Antes de continuarmos com o estudo da fução lambda, temos que atentar para a linha de
código 35, onde há a interface “Predicate” que é uma “interface funcional” que apresenta um
método a ser implementado, em outras palavras, um método abstrato. Isso significa que toda
interface criada que respeite esta premissa, tornando-se automaticamente uma interface funcional.
Também é possível ter um ou dois métodos “default”.

O compilador reconhece essas interfaces e permite que elas estejam disponíveis para que
os desenvolvedores trabalhem, por exemplo, classes anônimas e expressões lambda.

Predicado representa uma função de valor booleano que recebe um argumento e retorna um
resultado booleano.
105

Esta interface é comumente usada para definir condições ou filtros ao trabalhar com
coleções ou executar outras verificações condicionais, como no caso do exemplo da figura anterior.
A interface Predicate tem um método abstrato que é test(T t), utilizado na linha 37 da figura na
figura anterior.

O método avaliaExpressao, na linha 35, recebe como parâmetro uma lista e um objeto do
tipo Predicate, que é uma interface que espera uma função lógica, isto é, que avalia uma expressão
booleana, e retorna true ou false. Essa função é executada chamando o método “test” que executará
a função passada como parâmetro. Ao ser avaliada, se essa função retornar verdadeiro, imprime o
valor da lista, caso contrário, não imprime nada.

Considere outro exemplo:

A figura acima exibe um código parecido, mas em vez de um Predicate, a função passada
como parâmetro para o método é um IntFunction que espera funções que são realizadas sobre
números inteiros, como soma e multiplicação. No exemplo, é possível observar que, dentro do
método realizaOperacao, o objeto function chama o método apply, que executa a função lambda
106

passada como parâmetro e, assim como o método anterior, permite muita flexibilidade. Podemos
fazer qualquer tipo de operação sobre uma lista de números inteiros.

Assim como o Predicate e o IntFunction, existem diversas outras interfaces funcionais que
podem ser utilizadas com as funções lambada. Todas elas têm o mesmo objetivo, que é receber uma
função como parâmetro, mas a diferença entre essas interfaces são o tipo e o número de parâmetros
de entrada esperados e o tipo de retorno da função.

Mas é importante lembrar que as funções lambda nem sempre são a melhor opção, caso
seja necessário reutilizar diversas vezes uma função, talvez seja melhor criar uma classe ou
interface com apenas um método e não uma expressão lambda.

16. Métodos default


Com a introdução desse recurso na linguagem Java, obteve-se uma solução simples e que
reduz bastante o esforço de programação, simplificando o código ao possibilitar a criação de
interfaces que já possuam uma implementação padrão para alguns de seus métodos (mesmo que
sejam para lançar apenas uma exceção, por exemplo). Assim, quando uma classe implementar uma
interface, mas não sobrescrever seus métodos, será utilizado exatamente o código que foi definido
como implementação padrão na interface.

Veja exemplo na figura abaixo:

1. Linhas 2 e 6: Ao declarar as assinaturas dos métodos da interface IAceleracao, a cláusula


default (retângulos em branco) foi adicionada. Isso sinaliza o uso do recurso “Default
Methods”, permitindo adicionar uma implementação padrão dentro da própria interface para
cada método;

2. Linhas 15 a 29: Ao declarar a classe Maverick, optamos por seguir o padrão conhecido na
107

linguagem Java, implementando os métodos da interface; e

3. Linhas 31 e 33: Diferentemente do que é de costume, nas classes Gol e Fiesta os métodos da
interface não foram implementados. Contudo, como esses métodos estão marcados como
default, isso não gerará erro durante a compilação.

Nesse exemplo é perceptível a facilidade obtida ao acrescentar a cláusula default antes da


assinatura dos métodos da interface. Isso possibilita acrescentar uma rotina padrão que
normalmente só seria definida nas classes que a implementassem. Esse código padrão será
executado sempre que o compilador verificar, na classe, que esses métodos não foram
implementados.
108

17. Pacote [Link]


Assim como o restante das bibliotecas em Java, a parte de controle de entrada e saída de
dados (conhecido como “io”) é orientada a objetos e usa os principais conceitos apresentados até
agora: interfaces, classes abstratas e polimorfismo.

A ideia por trás do polimorfismo no pacote [Link] é de utilizar fluxos de entrada


(InputStream) e saída (OutputStream) para toda e qualquer operação, seja ela relativa a um arquivo,
seja relativa a um campo blob do banco de dados, a uma conexão remota via sockets, ou até mesmo
à entrada e saída padrão de um programa (normalmente o teclado e o console).

As classes abstratas InputStream e OutputStream definem, respectivamente, o


comportamento padrão dos fluxos em Java: em um fluxo de entrada, é possível ler bytes e, no fluxo
de saída, escrever bytes.

17.1. IOException

Quando trabalhamos com [Link], diversos métodos lançam IOException, que é uma
exception do tipo checked – o que nos obriga a tratá-la ou declará-la. Nos exemplos a seguir,
estamos declarando IOException por meio da cláusula throws do main apenas para facilitar o
exemplo. Neste caso, se uma exception ocorrer, a JVM parará, apresentando a “stacktrace”. Essa
não é uma boa prática em uma aplicação real: trate suas exceptions a fim de sua aplicação poder
abortar elegantemente.

17.2. InputStream

InputStream tem diversas outras filhas, como ObjectInputStream, AudioInputStream,


ByteArrayInputStream, FileInputStream, entre outras.

17.2.1. FileInputStream

Enquanto a classe InputStream é abstrata, FileInputStream é uma de suas filhas concretas.


FileInputStream procurará o arquivo no diretório em que a JVM fora invocada. Alternativamente,
você pode usar um caminho absoluto.

Portanto, com o objetivo de ler um byte de um arquivo, usaremos o leitor de arquivo


FileInputStream que, para conseguir ler um byte, precisa saber de qual “lugar” ele deverá ler. Essa
informação é tão importante que quem escreveu essa classe obriga você a passar o nome do arquivo
109

pelo construtor: sem isso, o objeto não pode ser construído. Exemplo:

class TestaEntrada {
public static void main(String[] args) throws IOException {
InputStream is = new FileInputStream(“[Link]”);
int b = [Link]();
}
}

17.3. Reader

Como Reader é uma classe abstrata, não é possível criar diretamente objetos desta classe. É
preciso criar objetos de uma de suas subclasses concretas para ter acesso às funcionalidades
especificadas por Reader. A documentação da API de Java mostra a seguinte hierarquia de classes
derivadas de Reader:

Algumas dessas classes indicam de onde os caracteres serão obtidos, ou seja, qual é a fonte
dos dados. São elas:

1. FileReader: permite associar a fonte de dados a um arquivo. Como arquivos disponibilizam


bytes, esta classe é uma especialização de InputStreamReader, que implementa uma ponte
para traduzir adequadamente bytes obtidos a partir de uma fonte binária para caracteres;

2. CharArrayReader e StringReader: permitem fazer a leitura desde arranjos de caracteres e de


objetos String, respectivamente; e

3. PipedReader: faz a leitura a partir de um objeto PipedWriter, estabelecendo um mecanismo


de comunicação inter-processos (no caso de Java, entre threads de uma mesma máquina
virtual).

Outras classes agregam funcionalidades a objetos Reader já existentes. Um


BufferedReader, por exemplo, incorpora um buffer a um objeto Reader, adicionando um método
para leitura de linhas. A classe LineNumberReader estende esta classe para fazer leitura de uma
fonte de caracteres por linhas e, ao mesmo tempo, manter um registro do número de linhas obtidas.
110

[Link]. InputStreamReader

InputStreamReader é filha da classe abstrata Reader, que tem diversas outras filhas – são
classes que manipulam chars.

[Link].1. Lendo de um arquivo

Para recuperar um caractere, precisamos traduzir os bytes com o encoding dado ao


respectivo código unicode, isso pode usar um ou mais bytes. Portanto, quem faz isso por você é a
classe InputStreamReader. Exemplo:

class TestaEntrada {
public static void main(String[] args) throws IOException {
InputStream is = new FileInputStream(“[Link]”);
InputStreamReader isr = new InputStreamReader(is);
int c = [Link]();
}
}
O construtor de InputStreamReader pode receber o encoding a ser utilizado como
parâmetro, se desejado, tal como UTF-8 ou ISO-8859-1. Temos três exemplos possíveis:
1. InputStreamReader in = new InputStreamReader([Link](),
StandardCharsets.UTF_8); e
2. InputStreamReader in = new InputStreamReader([Link](), “UTF_8”).
Apesar de a classe abstrata Reader já ajudar no trabalho de manipulação de caracteres,
ainda seria difícil pegar uma String. A classe BufferedReader é um Reader que recebe outro Reader
pelo construtor e concatena os diversos chars para formar uma String por intermédio do método
readLine. Exemplo:

class TestaEntrada {
public static void main(String[] args) throws IOException {
InputStream is = new FileInputStream(“[Link]”);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String s = [Link]();
}
}
Como o próprio nome diz, essa classe lê do Reader por pedaços (usando o buffer) para
evitar realizar muitas chamadas ao sistema operacional. Você pode até configurar o tamanho do
111

buffer pelo construtor. A figura abaixo apresenta a composição de classes que está acontecendo:

Esse padrão de composição é bastante utilizado e conhecido. É o Decorator Pattern.

No exemplo da figura abaixo, lemos apenas a primeira linha do arquivo. O método


readLine devolve a linha que foi lida e muda o cursor para a próxima linha. Caso ele chegue ao fim
do Reader (no nosso caso, final do arquivo), ele vai devolver NULL. Então, com um simples laço,
podemos ler o arquivo por inteiro:

[Link].2. Lendo do teclado

Com um passe de mágica, passamos a ler do teclado em vez de um arquivo, utilizando o


[Link], que é uma referência a um InputStream, o qual, por sua vez, lê da entrada padrão.
Exemplo:

Somente modificamos a quem a variável “is” está se referindo. Podemos receber


argumentos do tipo InputStream e ter esse tipo de abstração: não importa exatamente de qual lugar
estamos lendo esse punhado de bytes, desde que recebamos a informação a qual estamos querendo.
112

Como na figura:

Repare que a ponta da direita poderia ser qualquer InputStream, seja, um


ObjectInputStream, seja AudioInputStream, ByteArrayInputStream, ou a nossa FileInputStream.
Polimorfismo! Ou você mesmo pode criar uma filha de InputStream se desejar.

Por esse motivo, é muito comum métodos receberem e retornarem InputStream em vez de
suas filhas específicas. Com isso, elas desacoplam as informações e escondem a implementação,
facilitando a mudança e manutenção do código. Note: tudo isto vai ao encontro de tudo o que
aprendemos durante os capítulos que apresentaram as classes abstratas, as interfaces, o
polimorfismo e o encapsulamento.

[Link]. OutPutStream

Como você pode imaginar, escrever em um arquivo é o mesmo processo. Exemplo:

Exemplo de código:

O FileOutputStream pode receber um booleano como segundo parâmetro a fim de indicar


se você quer reescrever o arquivo ou manter o que já estava escrito (append).

O método write do BufferedWriter não insere o(s) caractere(s) de quebra de linha. Para
isso, você pode chamar o método newLine.
113

[Link]. Fechar arquivo

É importante sempre fechar o arquivo. Você pode fazê-lo chamando diretamente o método
close do FileInputStream / OutputStream, ou do BufferedReader / Writer. Neste último caso, o close
será cascateado para os objetos os quais o BufferedReader / Writer utiliza a fim de realizar a
leitura/escrita, além de ele fazer o flush dos buffers no caso da escrita.

É comum e fundamental que o close esteja dentro de um bloco finally. Se um arquivo for
esquecido aberto, e a referência a ele for perdida, pode ser que ele seja fechado pelo garbage
collector (que veremos mais à frente) por causa do finalize, mas não é bom você se prender a isso.
Se esquecer de fechar o arquivo, no caso de um programa minúsculo como esse, o programa
terminará antes que o tal do garbage collector o ajude, resultando em um arquivo não escrito (os
bytes ficaram no buffer do BufferedWriter). Problemas similares podem acontecer com leitores que
não forem fechados.

No Java 7, há a estrutura try-with-resources, que já fará o finally cuidar dos recursos


declarados dentro do try(), invocando close. Para isso, os recursos devem implementar a interface
[Link], que é o caso dos Readers, Writers e Streams estudados aqui:
114

18. Conversão
O primeiro caso é o de passar um número para uma string, e o jeito mais simples é
concatená-lo da seguinte maneira, conforme apresentam os exemplos abaixo:

A fim de transformar uma String em número, utilizamos as classes de ajuda para os tipos
primitivos correspondentes. Por exemplo, com o intuito de transformar a String ‘s’ em um número
inteiro, utilizamos o método estático da classe Integer:

As classes Double, Short, Long, Float, etc. contêm o mesmo tipo de método, como
parseDouble e parseFloat, que retornam um double e float respectivamente.
115

19. Classes wrapper


As classes Integer, Double, Short, Long, Float também são muito utilizadas para fazer o
wrapping (embrulho) de tipos primitivos como objetos, pois referências e tipos primitivos são
incompatíveis. Imagine que precisemos passar como argumento um inteiro para o nosso guardador
de objetos. Um inteiro não é um Object, como o faríamos? Exemplos:

E, dado um Integer, poderíamos pegar o int que está dentro dele (desembrulhá-lo).
Exemplo:

É possível efetuar estas operações diretamente, o que é chamado de “autoboxing”.


Exemplos:

Você pode fazer o autoboxing diretamente para Object também, possibilitando passar um
tipo primitivo a um método que receber Object como argumento. Exemplo: Object o = 5.
116

20. Threads
Em Java, usamos a classe Thread do pacote [Link] para criarmos linhas de execução
paralelas. A classe Thread recebe como argumento um objeto com o código que desejamos rodar.
Por exemplo, no programa de PDF e barra de progresso:

E, no método main, criamos os objetos e passamos para a classe Thread. O método start é
responsável por iniciar a execução da Thread:

O código acima, porém, não compilará. Como a classe Thread sabe que deve chamar o
método certo? Como ela sabe qual nome de método daremos e que deve chamar esse método
especial? Falta, na verdade, um contrato entre as nossas classes a serem executadas e a classe
Thread.

Esse contrato existe e é feito pela interface Runnable: devemos dizer que nossa classe é
executável e segue esse contrato. Na interface Runnable, há apenas um método chamado run. Basta
implementá-lo, assinar o contrato, e a classe Thread já saberá executar nossa classe. Exemplo:

A classe Thread recebe no construtor um objeto que é um Runnable, e seu método start
117

invoca o método run da nossa classe. Repare que a classe Thread não sabe qual é o tipo específico
da nossa classe; para ela, basta saber que a classe segue o contrato estabelecido e tem o método run.
É o bom uso de interfaces, contratos e polimorfismo na prática!

20.1. Thread x Runnable

A classe Thread implementa Runnable. Então, você pode criar uma subclasse dela e
reescrever o run que, na classe Thread, não faz nada:

E, como nossa classe é uma Thread, podemos usar o start diretamente:

Apesar de ser um código mais simples, você está usando herança apenas por preguiça
(herdamos um monte de métodos, mas usamos apenas o run), e não por polimorfismo, que seria a
grande vantagem. Prefira implementar Runnable a herdar de Thread.

20.2. Escalonador e trocas de contexto

Veja a classe a seguir:

“Programa” é uma classe que implementa Runnable e, no método run, apenas imprime dez
mil números. Vamos usá-la duas vezes para criar duas Threads e imprimir os números duas vezes
simultaneamente:
118

Se rodarmos esse programa, qual será a saída? De um a mil e depois de um a mil?


Provavelmente não, senão seria sequencial. Ele imprimirá 0 de t1, 0 de t2, 1 de t1, 1 de t2, 2 de t1, 2
de t2, etc.? Exatamente intercalado?

Na verdade, não sabemos exatamente qual é a saída. Rode o programa várias vezes e
observe: em cada execução, a saída é um pouco diferente.

O problema é que no computador existe apenas um processador capaz de executar coisas.


O que ocorre quando queremos executar várias coisas ao mesmo tempo, e o processador só
consegue fazer uma coisa de cada vez? Entra em cena o escalonador de Threads.

O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez,
pega todas as Threads que precisam ser executadas e faz o processador ficar alternando a execução
de cada uma delas. A ideia é executar um pouco de cada Thread e fazer essa troca tão rapidamente
que há a impressão de que as coisas estão sendo feitas ao mesmo tempo.

Ou seja, o escalonador é responsável por escolher qual a próxima Thread será executada e
fazer a troca de contexto (context switch).

Quando fizer a troca de contexto, por quanto tempo a Thread rodará e qual será a próxima
Thread a ser executada são escolhas do escalonador. Nós não as controlamos (embora possamos dar
dicas ao escalonador). Por isso, nunca sabemos, ao certo, a ordem em que programas paralelos são
executados.

Todo esse processo é feito automaticamente pelo escalonador do Java e, mais amplamente,
pelo escalonador do sistema operacional. Para nós, programadores e usuários das Threads, é como
se as coisas estivessem sendo executadas ao mesmo tempo.
119

20.3. Concorrência

Às vezes, um programa pode apresentar áreas compartilhadas por várias threads em que a
operação delas pode alterar dados, etc., enquanto uma está lendo estes dados. Esta parte do código
em um programa é chamada de região crítica, e o acesso a esta região gera o que é chamado de
concorrência.

20.3.1. Região crítica

Região crítica é uma parte de código que não pode ser executada por duas Threads ao
mesmo tempo, portanto, gera a concorrência, porque apenas uma thread por vez consegue entrar em
alguma região crítica.

Para solucionar o problema da concorrência, podemos usar o conceito de “trava”, em que,


no momento em que uma Thread acesse uma região crítica (que pode ser um método, por exemplo),
ela “tranca” a entrada com uma chave. Desta maneira, mesmo sendo colocada de lado, nenhuma
outra Thread poderia entrar nesses métodos, pois a chave estaria com a outra Thread.

É possível implementar esta lógica em Java, utilizando qualquer objeto como um lock
(trava ou chave) para poder sincronizar em cima desse objeto, isto é: se uma Thread entrar em um
bloco que foi definido como sincronizado por esse lock, apenas uma Thread poderá estar lá dentro
ao mesmo tempo, pois a chave estará com ela.

A palavra-chave synchronized implementa essa característica a um bloco de código e


recebe o objeto que será usado como chave. A chave só é devolvida quando a Thread que a tinha
sair do bloco, seja por return, seja por disparo de uma exceção ou apenas porque a execução do
bloco sincronizado terminou. Considere os exemplos da figura abaixo:

Observe o uso dos blocos synchronized dentro dos dois métodos. Eles bloqueiam uma
Thread utilizando o mesmo objeto Conta, o “this”.
120

Esses métodos são mutuamente exclusivos e só executam de maneira atômica. Threads que
tentam obter um lock que já foi obtido, ficarão em um conjunto especial esperando pela liberação
do lock (não necessariamente em uma fila).

[Link]. Sincronizando um bloco

É comum sempre sincronizarmos um método inteiro utilizando o “this” normalmente.


Exemplo:

[Link]. Sincronizando o método

Para o mesmo efeito, existe uma sintaxe mais simples, na qual o synchronized pode ser
usado como modificador do método. Exemplo:
121

21. Memória
Quando temos um dado no seu programa, você tem um conjunto de zeros e uns, mas esse
conjunto não pode simplesmente ficar solto no espaço - ele tem que estar bem localizado na
memória. Caso contrário, seu programa não saberia distinguir a folha de pagamento que você
implementou de fotos carregadas numa aba aberta do seu navegador.

21.1. Heap x stack

Em geral, runtimes como do C++, Java e “.NET” separam dois espaços na memória RAM
(literalmente algo do tipo “do byte de número X até o byte de número Y”) para cada programa que
estiver rodando. Estes dois espaços são, geralmente, chamados de Stack e Heap, mas estes nomes
são abstrações, convenções humanas, não de máquina.

Todos os objetos que você instancia em seu programa são alocados e endereçados na
memória, em um destes dois espaços. Se ele vai para o Stack ou para o Heap depende de como o
objeto é criado.

Objetos ditos por valor (em geral, os primitivos como int, bool, double etc.), bem como as
referências declaradas dentro de um método, são alocados no Stack. Objetos por referência vão para
a Heap.

21.1.1. Heap

Novamente, é necessário entendermos que estamos falando de um conceito, uma abstração


que é apenas um nome para facilitar nosso entendimento. Não existe um componente de hardware
chamado heap localizado na memória, nem mesmo uma área específica da memória onde “fica” o
heap. Ele está espalhado por toda a memória.

De forma simples, o heap (amontoar) é a área da memória em que os objetos da sua


aplicação são armazenados, mas não qualquer tipo de objeto, apenas aqueles criados a partir de
classes, ou seja, os objetos com semântica de referência.

É uma área de memória gerenciada pelo Garbage Colector (veremos mais adiante). Ela vai
ocupando espaços conforme a necessidade e o GC, em algum momento futuro, vai liberando os
espaços quando os dados contidos em uma determinada porção não são mais referenciados. A
liberação não é feita logo após o objeto não ser mais necessário.

A forma como ele é usado é sob demanda, ou seja, vai alocando espaços específicos
122

conforme a necessidade. No caso da JVM, boa parte desta alocação já é reservada com antecedência
pelo GC. Ele tenta administrar a memória da melhor forma possível.

Considere o seguinte exemplo: ArrayList x = new ArrayList().

Temos, na verdade, dois objetos. ‘x’ é uma referência (ou “ponteiro”) que corresponde a
um endereço na memória, ou seja, um tipo primitivo, portanto, vai para o Stack, pois a sua única
função é informar ao programa em que endereço no Heap se encontra a lista que você criou com o
comando new List(). Essa lista sobreviverá ao fim do escopo onde foi declarada.

Um motivo pelo qual é bom ter objetos no Heap é que você não precisa copiá-los inteiros
de uma chamada de método para outra. Por exemplo, se você tiver uma lista de 1 Mega e invocar
Foo(x) - aproveitando a que criamos mais acima – e ela fosse “por valor” em vez de ser por
referência, uma nova cópia da lista seria gerada toda vez que ela fosse passada como parâmetro,
então, seria o dobro de memória consumida. No final das contas, trabalhar com referências
economiza memória na maioria dos casos.

É mais rápido alocar e desalocar objetos no stack, mas eles só vão existir dentro do escopo
de um método. Já no Heap, existe gerenciamento de memória, e é mais lento alocar e desalocar, mas
os objetos sobrevivem ao método e morrem apenas quando não são mais referenciados.

21.1.2. Stack

Objetos curtos e de curta duração que são armazenados diretamente com seu valor ficam
na stack. Este é o caso dos objetos de tipos primitivos e será quando a linguagem tiver outros tipos
que não são nem primitivos nem criados com classes. Algumas pessoas esquecem que os dados
primitivos também são objetos (nem é porque Java é orientada a objeto, na verdade, é um outro
conceito de objeto). Talvez até porque Java não costume usar o termo objeto para estes dados, mas
eles são objetos. Em C, que não é nada orientada a objeto, também os chamamos de objetos.

Stack significa “pilha”. O Stack é feito de células, ou contextos. Toda vez que você invoca
um método, uma nova célula ou contexto é empilhada no stack com as informações daquele
método. Essa célula ou contexto existe enquanto o método não chegar ao seu fim, e todas as
variáveis que o método precisa são alocadas nessa célula.

É uma área de tamanho fixo de memória, normalmente, contígua em que os dados vão
sendo empilhados conforme eles precisam ser alocados e desempilhados quando não mais são
necessários. A ordem de desempilhamento deve ser, obrigatoriamente, inversa ao empilhamento.
Isto torna esta área muito rápida, mas sua limitação é ter um espaço que não pode crescer e que não
123

pode destruir dados de forma aleatória.

O Garbage Collector não tem poder nenhum aqui. Ele nunca vai desalocar os objetos na
stack. No entanto, quando um método acaba, sua célula no Stack é desempilhada e destruída, o que
acaba desalocando tudo que estiver lá.

21.2. Garbage collector

O Garbage Collector (coletor de lixo/lixeiro) funciona como uma Thread responsável por
jogar fora todos os objetos que não estão sendo referenciados por nenhum outro objeto – seja de
maneira direta, seja de maneira indireta. Como exemplo, considere o código apresentado na figura
abaixo:

Até esse momento, sabemos que existem dois objetos em memória. Aqui, o Garbage
Collector não pode eliminar nenhum deles, pois ainda tem variáveis se referindo a eles de alguma
forma, mas e se escrevermos a seguinte linha de código: conta2 = conta1.

Perdemos a referência a um dos objetos que foi criado: conta2, agora, “aponta” para o
mesmo objeto que conta1, na memória heap. Assim, o segundo objeto perdeu a sua referência. Ou
seja: já não é mais acessível.

Temos, então, apenas um objeto em memória? Não podemos afirmar isso. Como o
Garbage Collector é uma Thread, você não tem garantia de quando ele rodará. Você só sabe que, em
algum momento no futuro, aquela memória será liberada.

Outra maneira de fazer um objeto na memória heap perder a sua referência é como
demonstra o código a seguir: conta2 = null. Neste exemplo, a variável “conta2” não aponta para
nenhum objeto da heap.

21.2.1. [Link]()

Você nunca consegue forçar o Garbage Collector, mas, invocando o método estático gc da
classe System, está sugerindo que a Virtual Machine rode o Garbage Collector naquele momento.
Se sua sugestão será aceita ou não, isso depende de JVM para JVM, e não há garantias. Evite o uso
desse método, pois você não deve basear sua aplicação em quando o Garbage Collector irá rodar ou
não.
124

21.2.2. Finalizer

A classe Object define também um método finalize que você pode reescrever. Esse método
será invocado no instante antes do Garbage Collector coletar esse objeto. Não é um destrutor, você
não sabe em que momento ele será chamado. Algumas pessoas o utilizam para liberar recursos
caros como conexões, Threads e recursos nativos. Isso deve ser utilizado apenas por segurança: o
ideal é liberá-los o mais rápido possível sem depender da passagem do Garbage Collector.

Página 276.
125

22. Apêndice
Este capítulo apresenta material complementar ao conteúdo principal dessa apositla.

22.1. Composição

Além de estudarmos a composição em Java, vamos complementar esse estudo com o de


Agregação. A principal diferença entre Composição e Agregação está na força do vínculo e na
dependência do ciclo de vida entre os objetos. Ambas são formas de associação “tem um”, mas
funcionam conforme enumerado abaixo:

Composição (Vínculo Forte): na composição, o objeto “filho” pertence exclusivamente ao


objeto “pai” e não pode existir sem ele. Ou seja, estão intimamente ligados. Se o objeto principal for
destruído, o objeto composto também é destruído. Exemplo: um objeto de uma classe Quarto em
um objeto de uma classe Casa: se o objeto “casa” for demolida, o objeto “quarto” deixa de existir
também. Portanto, na implementação, o objeto atributo (parte) é instanciado dentro da classe
principal. A figura x apresenta um exemplo da composição descrita antes.

Figura 20: exemplo de composição.

Fonte: Gemini.

Na agregação, o objeto “filho” pode existir de forma independente do objeto “pai”. Ou


seja, são independentes. O objeto atributo (parte) pode ser compartilhado por diferentes objetos
principais. Exemplo: um funcionário em um departamento. Se o departamento for extinto, o
funcionário ainda existe e pode ir para outro lugar. Portanto, na implementação, o objeto parte é
criado externamente e passado para a classe (via construtor ou setter). A figura x apresenta um
exemplo de agregação.
126

Figura 21: exemplo de agregação.

Fonte: Gemini.

Por fim, a tabela x apresenta um comparativo entre composição e agregação.

Tabela 22.1: Composição vs. Agregação.

Fonte: Gemini.

Retornar ao subcapítulo Atributos.

22.2. Acoplamento

Em Java, acoplamento é o nível de dependência ou interdependência entre classes,


módulos ou componentes de um sistema. Ele mede o quanto uma classe “conhece” ou precisa de
outra para funcionar corretamente.

O objetivo no desenvolvimento Java de qualidade é buscar o baixo acoplamento (ou


acoplamento fraco), pois ele torna o código mais flexível, fácil de testar e manter. As diferenças são
enumeradas abaixo:

1. Acoplamento Forte (Tight Coupling): ocorre quando uma classe depende diretamente de
detalhes internos ou de implementações concretas de outra classe. Problema: se você alterar
a classe ‘A’, provavelmente, terá que alterar a classe ‘B’ também. Isso cria um efeito dominó
de erros. Exemplo: criar uma instância de uma classe dentro de outra usando o operador new
(private Motor motor = new MotorDiesel();); e
127

2. Acoplamento Fraco (Loose Coupling): ocorre quando as classes interagem através de


interfaces ou abstrações, sem conhecer os detalhes de como a outra classe funciona
internamente. Exemplo: receber a dependência pelo construtor via interface (public
Carro(Motor motor) { ... }). A vantagem dessa “abordagem” é poder substituir uma
implementação por outra sem quebrar o restante do sistema.

Existem boas práticas de como reduzir o acoplamento em Java para criar sistemas mais
robustos em que os desenvolvedores utilizam técnicas como:

1. Programar para Interfaces: em vez de depender de classes concretas, dependa de interfaces;

2. Injeção de Dependência (DI): deixar que um framework (como o Spring) ou o próprio


código injete as dependências necessárias, em vez de a classe criá-las internamente; e

3. Encapsulamento: esconder os detalhes de implementação e expor apenas o necessário via


métodos públicos.

Retornar à figura exemplo de acoplamento.

22.3. Questões

Questão 1: Conceitos de Orientação a Objetos

No ambiente de programação Java, sobre classes e interfaces, é correto afirmar:

a. Uma classe abstrata permite apenas a definição de métodos abstratos.

b. O corpo de um método abstrato termina obrigatoriamente com ponto e vírgula e sua


declaração é delimitada por chaves.

c. Uma interface pode definir tanto métodos abstratos quanto métodos com
implementação padrão (default methods).

d. A herança múltipla de classes é permitida, possibilitando que uma classe estenda


várias outras simultaneamente.

e. Nem toda classe em Java é, necessariamente, uma subclasse da classe Object.

Questão 2: Arrays e Estruturas de Repetição (pesquisar “jagged array”).

Considere o seguinte código Java apresentado na figura x.


128

Figura 22: questão 2.

Fonte: Gemini.

Sobre a execução deste código, a saída exibida será:

a. 0 1 1 2

b. 0 1 2 3

c. Ocorrerá uma exceção NullPointerException.

d. 1 2 2 3

e. O código não compila devido à inicialização do array dados.

Questão 3: Frameworks e Testes

Para realizar testes de unidade em aplicações Java, garantindo que métodos de classes
produzam os resultados esperados de forma isolada ou em baterias automatizadas, o framework
open-source mais popular utilizado é o:

a. JMeter

b. JUnit

c. Log4j

d. Hibernate

e. Spring Security

Gabarito: C, A, B

Questão 4: Tratamento de Exceções

Considere o seguinte bloco de código em Java apresentado na figura x.


129

Figura 23: questão 4.

Fonte: Gemini.

Ao ser executado, o que será impresso no console?

a. A

b. B

c. BC

d. AC

e. O código não compila.

Questão 5: Polimorfismo e Classes

Observe as classes apresentadas na figura x.

Figura 24: questão 5.

Fonte: Gemini.

Qual o resultado da execução do método main?


130

a. Som

b. Latido

c. Som Latido

d. Ocorrerá um erro de compilação na linha Animal pet = new Cao();.

e. Ocorrerá um erro de execução (Runtime Exception).

Questão 6: Manipulação de Strings (Imutabilidade)

Analise o código Java apresentado na figura x.

Figura 25: questão 6.

Fonte: Gemini.

O que será exibido no console?

a. TRT4

b. TRT 4

c. TRT

d. 4

e. Erro de compilação.

Gabarito: C, B, C

Quanto à questão 6, vale lembrar: Strings em Java são imutáveis. O método concat() não
altera a string s1; ele cria uma nova string com o resultado. Como o código não atribuiu esse
resultado de volta para s1 (ex: s1 = [Link]("4")), o valor original de s1 permanece inalterado.

Mais questões:

1) [Link]
q=exemplos+de+quest%C3%B5es+de+Java%2C+da+Funda%C3%A7%C3%A3o+Carlos+Chagas
%2C+para+o+TRT4.&rlz=1C1CHBD_pt-
131

PTBR911BR911&sourceid=chrome&ie=UTF-8&aep=48&cud=0&qsubts=1776997116872&source
=[Link]&mstk=AUtExfDKH9_sX-Xr96vpTk8rlAifd0S85QkkMQ3mE2GMP2ZruTf-
GI6pssi7_vtVPI7FKh86asaI80ZllexbWJxmfeEm9ncA9iuiDgXSDhY-
UdNqku3DHvSc2zv5zJ0FXhHOOdvN3yDwOjCESdNTD4xPzJjfZo0UUC3E97TNRrcdoy5mqq9P
jWKZEwyXroVDL0RvMfoPrT45XPyG28gKHZg8Lzu1KH4E2MdbPWaP6Vl4zcKiWnFZFO9Yw
7Psaw&csuir=1&udm=50

2) [Link]
q=O+que+s%C3%A3o+Wildcards%2C+em+Java%3F&rlz=1C1CHBD_pt-
PTBR911BR911&sourceid=chrome&ie=UTF-8&aep=48&cud=0&qsubts=1777001023704&source
=[Link]&udm=50&mstk=AUtExfDsjZ1DeyqiKYvL9If4CbebTtlxfdcZHF3uI4x1SdX3Wo
H9GQxZMn30XKNFGaxX6NWt4HdDILKUI6ceUPIq5lEFuZK-o_EPR-
q-6BrGKNs7NfpgmTBsvC-
mnZvJDtJ3YDrIrNthr7AP4XA8s6c9WbY3wBleu6q5JH2Cc_8k5vcvDC2Y58RkHiGgKfDGhtmM
CsXYKp255SnS-9ur0-sUzdsAWp70pdQ8oUA2HDqN78i0Mc4iHt_IfpiYcQ&csuir=1

Você também pode gostar