String
Já usámos um tipo de dados não primitivo! É o tipo String
que é definido através da classe String
.
Mas usámo-lo de forma muito básica, sem aproveitar o seu grande potencial. Vamos agora perceber porque é que o tipo String
é tão importante na construção de programas.
Se as String
s são objetos, podemos perguntar
String
?” e Antes de respondermos a estas questões, vamos primeiro falar sobre mais um tipo de dados primitivo, fundamental para abordarmos as strings.
char
Os componentes de um objeto do tipo String
são letras, algarismos ou outros carateres.
Cada um desses carateres, separadamente, pode ser visto como um valor do tipo char
– um outro tipo de dados primitivo que ainda não tínhamos visto e que representa carateres individuais.
Os literais deste tipo são definidos entre pelicas, como em 't'
, '3'
e '#'
.
Um exemplo de utilização de char
:
xchar letra = 'w';
if (letra != '&') {
System.out.println(letra);
}
String
Existem muitos métodos definidos no tipo String
, mas vamos estudar só alguns.
Pode ver o resto em https://docs.oracle.com/javase/9/docs/api/java/lang/String.html, onde encontra a documentação completa da classe String
.
O método charAt
permite conhecer um caráter numa dada posição de uma String
. Aplicado sobre uma dada String
, devolve um valor do tipo char
. A assinatura do método charAt
é:
public char charAt(int index)
e a descrição sucinta é: “Returns the char
value at the specified index.”
Um exemplo de utilização do método charAt
:
xxxxxxxxxx
String animal = "tigre";
char letra = animal.charAt(1);
System.out.println(letra);
A expressão animal.charAt(1)
significa que estamos a invocar o método charAt
sobre a String
contida na variável animal
. O resultado é um caráter que é de seguida guardado numa variável do tipo char
chamada letra
.
Que letra pensa que iremos obter com a expressão animal.charAt(1)
? Se calhar estava à espera que aparecesse a primeira letra de “tigre” e afinal aparece a letra i
...
Se tivesse lido com atenção a documentação do método charAt
, na API da classe String
, teria percebido que:
- as posições numa
String
são numeradas a partir de zero e, por isso, a letra na posição 1 daString
é a segunda letra daString
.No geral, o caráter na posição n de uma string é o seu (n+1)-ésimo caráter.
Então, se queremos a primeira letra de uma String
, temos que dar o valor zero ao parâmetro do método:
xxxxxxxxxx
char letra = animal.charAt(0);
Repare bem na forma como invocámos o métod charAt
– está precedido por animal.
, isto é,
String
É assim que dizemos que queremos que o método charAt
“trabalhe” sobre a String
guardada na variável animal
. Se invocarmos o mesmo método sobre uma String
diferente, o seu resultado é potencialmente diferente.
As instruções que se seguem exemplificam como o mesmo método, invocado com o mesmo valor para o parâmetro, sobre três String
s diferentes, devolve três resultados diferentes.
xxxxxxxxxx
char c1 = animal.charAt(0);
char c2 = "papagaio".charAt(0);
String outro = "baleia";
char c3 = outro.charAt(0);
A variável c1
fica com valor 't'
, c2
com o valor 'p'
e c3
com o valor 'b'
.
Lembra-se dos métodos da classe Math
? A forma como invocamos esses métodos é escrevendo Math.
seguido do nome do método e dos valores para os parâmetros (se for caso disso), como no caso do cálculo da raiz quadrada de 7:
xxxxxxxxxx
double raiz = Math.sqrt(7);
Temos que fazer assim porque todos os métodos na classe Math
são definidos como static
, significando que são métodos de classe.
Os métodos de classe (
static
) não são invocados sobre nenhum objeto em particular – o seu resultado depende unicamente dos valores dados para os parâmetros.
De facto, ao contrário da classe String
, a classe Math
não tem objetos (ou instâncias).
Se inspecionar a API da classe String
, verificará que o método charAt
não é static
, nem a maior parte dos métodos dessa classe.
Isto significa que têm que ser invocados sobre objetos específicos do tipo String
e que o seu resultado depende não só dos valores dados para os parâmetros, mas também das características específicas desses objetos (o seu estado).
Os métodos que não são
static
dizem-se métodos de instância pois são invocados sobre um objeto ou instância da classe em que estão definidos.Dizemos que o objeto sobre o qual o método é invocado é o alvo da invocação.
String
O método que, na classe String
, permite conhecer o comprimento de uma String
é o método de instância length
que, quando invocado sobre uma dada String
retorna um inteiro representando o comprimento dessa String
. Não tem parâmetros. Exemplo:
xxxxxxxxxx
int quantos = animal.length();
System.out.println( "papagaio".length() );
A variável quantos
ficará com o valor 5 e o valor 8 será escrito no ecrã.
Para conhecer a última letra de uma string poderá sentir-se tentado a experimentar algo como:
xxxxxxxxxx
char ultimo = animal.charAt( animal.length() ); // ERRADO!!!
Isto não funciona. A razão é que não existe uma letra na posição cinco em "tigre". Como o primeiro caráter de uma String
é o que está na posição zero, então as cinco letras de "tigre" estão nas posições zero a quatro. Para obtermos o último caráter, temos que subtrair 1 de length
.
xxxxxxxxxx
char ultimo = animal.charAt( animal.length() - 1 ); // CERTO
String
É muito comum precisarmos de percorrer, um a um, os carateres de uma String
usando-os para algum objetivo. Uma forma natural de fazer essa travessia é usando uma instrução for
:
xxxxxxxxxx
String palavra = "papagaio";
for (int index = 0 ; index < palavra.length() ; index++) {
char letra = palavra.charAt(index);
if (letra == 'a') {
System.out.print('@');
} else {
System.out.print(letra);
}
}
Este ciclo atravessa a String
contida na variável palavra
e imprime cada um dos seus carácteres no ecrã ou @
se o caráter for a
. Os carateres seguintes serão escritos no ecrã:
xxxxxxxxxx
p@p@g@io
Repare que a guarda é index < palavra.length()
, o que significa que, quando a variável index
é igual ao comprimento da String
, a guarda é falsa e o corpo do ciclo já não é executado – o último caráter que acedemos é o que está na posição palavra.length() - 1
, que é exatamente o que pretendemos.
Escolhemos index
para o nome da variável de progresso do ciclo. Um índice ou index é um valor usado para identificar um elemento de um conjunto ordenado, neste caso a String
de carateres. O índice indica (daí o nome) qual o elemento que pretendemos.
Na Secção 2.4.2 falámos sobre erros de execução, erros que só aparecem quando um programa já está em fase de execução. Em alguns casos os erros provocam aquilo a que se chama, em Java, de exceções, que podem ajudar o programador a corrigir o seu programa.
O método charAt
provoca uma exceção quando é invocado com um valor para o parâmetro que é negativo ou maior ou igual ao comprimento da String
sobre o qual é invocado.
Quando isso acontece, o Java imprime uma mensagem de erro indicando o tipo da exceção que ocorreu e a lista dos métodos que estavam em execução nessa altura (stack trace).
Um exemplo:
xxxxxxxxxx
public class ExemploErroExecucao {
public static void main(String[] args) {
imprimeUltima("papagaio");
imprimeUltima("tigre");
}
public static void imprimeUltima(String s) {
char c = ultimaLetra(s);
System.out.println(c);
}
public static char ultimaLetra(String s) {
int index = s.length();
char c = s.charAt(index); // ERRO!
return c;
}
}
Repare no erro no método ultimaLetra
: o índice usado para ir buscar o último caráter devia ser index - 1
.
O programa compila sem problemas mas, quando executado, imprime:
xxxxxxxxxx
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 8
at java.lang.String.charAt(String.java:686)
at ExemploErroExecucao.ultimaLetra(ExemploErroExecucao.java:15)
at ExemploErroExecucao.imprimeUltima(ExemploErroExecucao.java:9)
at ExemploErroExecucao.main(ExemploErroExecucao.java:4)
e de seguida termina a sua execução. Esta lista (stack trace) pode ser difícil de ler, mas contém muita informação útil.
Mais para a frente veremos que o efeito das exceções não tem que ser sempre este. Há casos em que o programa pode "recuperar" de uma exceção, sem necessariamente ter que terminar.
indexOf
O método indexOf
é como que o inverso de charAt
: dado um índice, o método charAt
retorna o caráter que está na posição dada pelo índice; dado um caráter, o método indexOf
devolve o índice (posição) onde o caráter aparece.
Já viu que o método charAt
falha quando o índice está fora do intervalo permitido, lançando uma exceção. O método indexOf
não consegue determinar o índice de um caráter que não aparece na String
mas, em vez de lançar uma exceção, retorna o valor -1.
xxxxxxxxxx
int index1 = "papagaio".indexOf('t');
int index2 = "papagaio".indexOf('a');
Este pedaço de código atribui à variável index1
o valor -1 pois a letra 't'
não existe na String
"papagaio".
Atribui ainda à variável index2
a posição da letra 'a'
na String
"papagaio". Neste caso a letra aparece três vezes e por isso não é óbvio o resultado que indexOf
produz. De acordo com a documentação, o método retorna o índice da primeira ocorrência do caráter (nesta caso o valor 1, pois a letra 'a'
ocorre pela primeira vez na posição 1 da String
).
Para encontrar posteriores ocorrências de uma letra, pode usar-se uma outra versão de indexOf
, que, além do caráter a procurar, pede também a posição a partir da qual deve fazer a procura.
String
s são imutáveisNa documentação do método toUpperCase
da classe String
está escrito:
Converts all of the characters in this String
to upper case...
Podemos ser levados a pensar que o método altera a String
sobre a qual é chamado. Este é um engano comum a quem começa a usar esta classe.
Nenhum dos métodos da classe
String
altera asString
s sobre as quais são invocados
- os objetos do tipo String são imutáveis, isto é, o seu estado nunca muda.
No caso do método toUpperCase
, a sua invocação devolve uma nova String
. Por exemplo:
xxxxxxxxxx
String animal = "papagaio";
String animalMaiusculas = animal.toUpperCase();
Após a execução destas duas instruções, a variável animalMaiusculas
contém a String "PAPAGAIO", mas animal
ainda contém "papagaio".
String
s É muitas vezes necessário comparar String
s de modo a verificar se são iguais. As instruções seguintes
xxxxxxxxxx
String animal1 = "papagaio";
String animal2 = "PAPAGAIO".toLowerCase();
if (animal1 == animal2) {
System.out.println("Iguais");
} else {
System.out.println("Diferentes");
}
imprimem a palavra Diferentes embora os valores das variáveis animal1
e animal2
sejam textualmente iguais.
A razão para isso prende-se com o facto de as variáveis animal1
e animal2
serem do tipo String
, que é um tipo não primitivo.
Numa atribuição a uma variável de um tipo não primitivo, a expressão à direita da atribuição denota um objeto.
O conteúdo dessa variável passa a ser uma referência para esse objeto.
Por exemplo, a instrução seguinte atribui um objeto do tipo String
a uma variável do mesmo tipo (não primitivo):
xxxxxxxxxx
String s = "Blah";
Embora as figuras representando o conteúdo da memória que temos usado nestas páginas representem o conteúdo das variáveis do tipo String
como se de um tipo primitivo se tratasse, no entanto, a forma standard de representar graficamente o conteúdo de variáveis de tipos não primitivos é:
Como usualmente, o nome da variável s
aparece fora da caixa e o seu valor dentro da caixa. Neste caso, o valor é uma referência, representada graficamente por uma seta. A seta aponta para o objeto que a variável refere.
Agora percebe porque é que as instruções anteriores imprimem Diferentes em vez de Iguais?
Se não, repare: o conteúdo de cada uma das variáveis é uma referência para um objeto:
Embora os objetos sejam iguais, as referências não o são (pode imaginar uma referência a um objeto como a morada onde esse objeto “vive” em memória).
Como o operador relacional ==
compara o conteúdo das variáveis, o valor da expressão animal1 == animal2
é false.
É por esta razão que não se devem comparar expressões de tipo não primitivo usando o operador ==
. A comparação só dá true
quando se compara um objeto com ele próprio.
Então como podemos comparar
String
s?
- usando o método
equals
, que recebe como parâmetro um objeto e o compara com aString
sobre a qual o método é invocado;- o resultado será
true
se as duasString
s forem iguais, ou seja, se forem formadas pelos mesmos carateres, pela mesma ordem.
As seguintes instruções já imprimem a palavra Iguais:
xxxxxxxxxx
String animal1 = "papagaio";
String animal2 = "PAPAGAIO".toLowerCase();
if (animal1.equals(animal2)) {
System.out.println("Iguais");
} else {
System.out.println("Diferentes");
}
Anterior: 7.1. Tipos de dados não primitivos
Seguinte: 8. Algumas classes da biblioteca do Java