7.2. Revisitando o tipo 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 Strings são objetos, podemos perguntar

Antes de respondermos a estas questões, vamos primeiro falar sobre mais um tipo de dados primitivo, fundamental para abordarmos as strings.

7.2.1. O tipo de dados primitivo 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:

Experimente executar o programa:


 

7.2.2. Métodos definidos no tipo 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:

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)? Vamos executar o programa com estas instruções:

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:

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:

 

7.2.3. Métodos de instância versus métodos de classe

Repare bem na forma como invocámos o métod charAt – está precedido por animal., isto é,

É 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 Strings diferentes, devolve três resultados diferentes.

Experimente:

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:

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.

 

7.2.4. Comprimento de uma 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:

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:

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.

 

7.2.5. Travessia de uma 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:

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ã:

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.

Experimente executar:

 

 

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.

7.2.6. Erros de execução

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:

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:

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.

7.2.7. O método 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.

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.

7.2.8. As Strings são imutáveis

Na 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 as Strings sobre as quais são invocados

 

No caso do método toUpperCase, a sua invocação devolve uma nova String. Por exemplo:

Após a execução destas duas instruções, a variável animalMaiusculas contém a String "PAPAGAIO", mas animal ainda contém "papagaio".

7.2.9. Comparação de Strings

É muitas vezes necessário comparar Strings de modo a verificar se são iguais. As instruções seguintes

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):

Embora o "simulador" de execução de programas que temos usado nestas páginas represente 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 Strings?

As seguintes instruções já imprimem a palavra Iguais:

Pode experimentar executar o seguinte programa que usa esta versão do método:


 

Anterior: 7.1. Tipos de dados não primitivos

Seguinte: 8. Algumas classes da biblioteca do Java