Java Básico
Java Básico
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.
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
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?
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.
É 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.
“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.
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.
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.
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.
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.
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:
3.1.3. “boolean”
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:
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.
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:
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.
A mesma incompatibilidade ocorre no seguinte trecho: int i = 3.14. Nem mesmo o seguinte
código compila:
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.
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.
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.
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
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”.
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.
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
Tanto o “for” quanto o “while” e o “do while” são iguais às demais linguagens de
programação.
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:
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!
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.
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.
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.
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:
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:
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
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.
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
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
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.
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:
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”.
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;
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.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
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
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:
totalDeContas = totalDeContas + 1;
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:
4.9. Herança
Considere o seguinte caso de uma empresa que tem funcionários. Podemos modelar uma
classe Funcionário da seguinte forma:
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.
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.
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
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.
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
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:
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:
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
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.
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.
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:
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]”.
1. É muito importante manter a ordem! Primeiro, aparece uma (ou nenhuma) vez o “package”,
38
2. É possível importar um pacote inteiro (todas as classes do pacote, exceto os subpacotes) por
meio do coringa ‘*’. Exemplo: “import [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.
A classe System possui uma série de atributos e métodos estáticos. Já usamos o atributo
[Link] para imprimir.
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)”.
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.
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.
1. O nome da classe;
2. Um símbolo @; e
3. Um número de identidade.
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:
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
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.
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
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.
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:
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:
[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:
Existem diversos métodos da classe String que são extremamente importantes. Abaixo,
mais alguns exemplos:
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;
9. “contains”: pesquisar; e
10. “matches”:
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:
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
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
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.
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
À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á.
Ou seja, temos:
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.
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
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:
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.
Assim, podemos criar este "contrato" o qual define tudo o que uma classe deve fazer se
quiser ter um determinado status. Exemplo:
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.
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.
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.
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
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
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:
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:
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
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.
É 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.
Podemos transformar essa Exception de unchecked para checked, obrigando quem chama
esse método a dar try-catch ou throws. Exemplo:
63
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.
10. Arrays
Considere o seguinte exemplo: “double[] saldosDasContas;”.
É 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.
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:
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
Na figura acima, você pode ver um array que, antes, estava sendo utilizado e, depois, teve
um de seus elementos removidos.
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?
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:
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.
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.
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
[Link]("java");
[Link]("vraptor");
[Link]("scala");
[Link](palavra);
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.
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]();
1. hasNext() (com retorno booleano), indica se ainda existe um elemento a ser percorrido; e
Exemplo:
[Link]("item 1");
[Link]("item 2");
[Link]("item 3");
// retorna o iterator
Iterator<String> i = [Link]();
while ([Link]()) {
// recebe a palavra
[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
[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.
[Link](100);
[Link](200);
[Link](300);
[Link](c1);
[Link](c3);
[Link](c2);
[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:
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
[Link]([Link]());
Como em qualquer lista é possível colocar qualquer Object, é possível misturar objetos.
Exemplo:
[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
[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:
[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.
A classe Collections fornece um método estático sort , que recebe um List como argumento
[Link]("Sérgio");
75
[Link]("Paulo");
[Link]("Guilherme");
[Link](lista);
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:
[Link](500);
[Link](200);
[Link](150);
[Link](c1);
[Link](c3);
[Link](c2);
É 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!
Assim, para ordenar os objetos da classe ContaCorrente por saldo, por exemplo, basta
implementar o Comparable. Exemplo:
return -1;
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
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.
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.
[Link]("Gerente");
[Link]("Diretor");
[Link]("Presidente");
[Link]("Secretária");
[Link]("Funcionário");
[Link]("Diretor"); // repetido!
[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).
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 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.
“[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.
[Link](10000);
[Link](3000);
// cria o mapa
[Link]("diretor", c1);
81
[Link]("gerente", c2);
[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.
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;
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
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.
Fonte: [Link]
Com relação a complexidade computacional, a figura abaixo apresenta cada uma das
implementações da interface Map.
83
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.
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.
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.
12.2. TreeMap
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]
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.
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]
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.
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.
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
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]
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
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
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.
Fonte: [Link]
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".
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
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:
[Link]("[Link]", "scott");
[Link]("[Link]", "tiger");
[Link]("[Link]","jdbc:mysql:/localhost/teste");
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.
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.
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 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.
1. Classes internas: são classes não estáticas dentro de uma classe externa;
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
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.
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
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
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:
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
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
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
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.
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.
2. Linhas 15 a 29: Ao declarar a classe Maverick, optamos por seguir o padrão conhecido na
107
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.
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
17.2.1. FileInputStream
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:
[Link]. InputStreamReader
InputStreamReader é filha da classe abstrata Reader, que tem diversas outras filhas – são
classes que manipulam chars.
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:
Como na figura:
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
Exemplo de código:
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
É 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.
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
E, dado um Integer, poderíamos pegar o int que está dentro dele (desembrulhá-lo).
Exemplo:
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!
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:
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.
“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
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 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.
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.
É 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.
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).
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.
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
É 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.
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
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á.
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
Fonte: Gemini.
Fonte: Gemini.
Fonte: Gemini.
22.2. Acoplamento
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
Existem boas práticas de como reduzir o acoplamento em Java para criar sistemas mais
robustos em que os desenvolvedores utilizam técnicas como:
22.3. Questões
c. Uma interface pode definir tanto métodos abstratos quanto métodos com
implementação padrão (default methods).
Fonte: Gemini.
a. 0 1 1 2
b. 0 1 2 3
d. 1 2 2 3
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
Fonte: Gemini.
a. A
b. B
c. BC
d. AC
Fonte: Gemini.
a. Som
b. Latido
c. Som Latido
Fonte: Gemini.
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