Universidade Federal de Minas Gerais
Instituto de Ciências Exatas
Departamento de Ciência da Computação

Algoritmos e Estruturas de Dados I

A classe String define um tipo String

Uma boa parte dos programas de computador precisam representar e manipular  números e também seqüências de caracteres. As necessidades numéricas são, grosso modo, atendidas pelos tipos primitivos tais como long, int e double. As linguagens de alto-nível têm diferentes abordagens para lidar com a necessidade de representar e manipular as seqüências de caracteres. Na linguagem Java as seqüências de caracteres podem ser representadas utilizando arranjos de caracteres (char arrays) ou então as cadeias de caracteres (strings). Aqui iremos dar ênfase no suporte de processamento de texto utilizando cadeias de caracteres.

Quando discutimos arranjos vimos que eles são objetos especiais. Os arranjos possuem "inicializadores" e arranjos podem ser indexados. As cadeias de caracteres são objetos especiais também, mas em um sentido um pouco diferente. Uma cadeia de caractere  é o único objeto que possui uma representação no código fonte do programa, ou seja uma cadeia de caractere é o único objeto que tem um literal na linguagem Java.

Considere o seguinte trecho de programa:
System.out.println(123);
System.out.println(3.1415);
System.out.println("Ola!");

O tipo do literal 123 é int. o tipo do literal 3.1415 é double. Em Java int e double são tipos primitivos. O tipo da cadeia de caracteres "Ola!" é String. Em Java String é uma classe(class). A classe é o elemento da Linguagem Java que define a estrutura(estado) e o comportamento de seus objetos(instâncias). Mais a frente na disciplina iremos apresentar o conceito classe com mais detalhes. Neste ponto da disciplina iremos apenas estudar uma classe especial denominada String (uma classe pré-definida) cujos objetos ou instâncias contêm cadeias de caracteres. Uma classe define um tipo. A expressão “instanciar a classe C” deve ser interpretada da mesma forma que a expressão “criar um objeto da classe C” ou ainda “criar um objeto do tipo C”. Uma variável do tipo String é uma variável do tipo referência, ou seja, contém um endereço de objeto (instância). Uma variável de tipo primitivo P deverá conter um valor do tipo P, uma variável do tipo referência deverá conter um endereço de um objeto (instância) do tipo referenciado, em particular uma variável do tipo String deverá conter um endereço de um objeto do tipo String.

O trecho de programa:
String s;
corresponde a definir uma variável com nome s e tipo String, mas o conteúdo de s não é definido. O trecho de programa:
String s="abcde";
Corresponde a definir uma variável com nome s, tipo String e conteúdo correspondendo a um endereço de memória onde o compilador montou um objeto contendo a cadeia de caracteres "abcde".

Uma variável do tipo String pode ser iniciada ou inicializada de forma semelhante a uma variável de tipo primitivo além de poder ser iniciada através de construtores como será visto mais à frente. A iniciação é feita com literais do tipo String . Um literal do tipo String é uma seqüência de caracteres iniciada e terminada com aspas. Podemos utilizar o operador de concatenação de cadeias (+). O operador de concatenação permite representar uma cadeia em diferentes linhas do texto do programa. Exemplo de declaração de variáveis do tipo cadeia de caracteres já inicializadas:
String s1="Universidade ";
String s2="Federal de";
String s3=" Minas "+
          "Gerais";
String universidade=s1+s2+s3;

No trecho de programa acima a variável universidade é inicializada utilizando o operador de concatenação de cadeias de caracteres. Os literais de tipo String podem ser usados em comandos de atribuição:
String universidade;
universidade="Universidade Federal de Minas Gerais";

No trecho acima a variável universidade recebe o endereço de memória onde fica um objeto contendo a cadeia de caracteres "Universidade Federal de Minas Gerais". A figura abaixo mostra uma representação gráfica simplificada para o que discutimos acima:
modelo gráfico simples de um objeto String

Observe que a organização do bloco de memória correspondente a uma cadeia de caracteres é diferente da organização de um bloco de memória correspondente a um arranjo de caracteres. No caso de arranjo de caracteres cada caractere pode ser indexado através do operador [] (abre e fecha colchete), o operador de indexação não pode ser usado em cadeias de caracteres. A manipulação de caracteres individuais de uma cadeia de caracteres tem que ser feita através de métodos conforme explicado abaixo. Os métodos da classe String permitem vários tipos de operações sobre os caracteres e substrings (trechos de cadeias de caracteres).

O conteúdo da variável universidade é um endereço de um bloco de memória. Este bloco de memória é o objeto que contém a cadeia de caracteres. A distinção entre a cadeia de caracteres (seqüência de caracteres) e o objeto (o bloco de memória onde fica a seqüência de caracteres) não é muito reforçada em alguns textos. Um objeto é também referido como uma instância, o objeto acima é uma instância da classe String. Não são mostrados todos os campos do objeto, apenas um campo (este campo com uma referência para a classe String não é disponível para o programador!)  e que contém uma referência para a classe String. A classe String não é detalhada, mas uma classe pode definir campos e métodos (methods), dentre outros elementos.

Um método corresponde a um código que implementa alguma funcionalidade relacionada à classe ou a um objeto. Os métodos que implementam uma funcionalidade relacionada à classe são denominados métodos estáticos (static) ou métodos de classe e os métodos que implementam uma funcionalidade relacionada ao objeto são denominados não-estáticos ou métodos de instância. Uma chamada ou invocação de método corresponde a um comando que altera o fluxo de controle. Uma chamada ou invocação de método desvia o fluxo de controle para o código de máquina do método, um mecanismo, denominado mecanismo de retorno, permite, terminada a execução do método, que o controle retorne ao comando seguinte à chamada. Os métodos estáticos são invocados utilizando o nome da classe seguido do operador '.' seguido do nome do método. Os métodos não-estáticos são invocados utilizando uma referência para o objeto seguido do operador '.', seguido do nome do método. Abaixo são mostrados alguns dos métodos da classe String:

O trecho de programa abaixo imprime o comprimento, em termos de números de caracteres, de uma cadeia de caracteres referenciada pela variável cordao:
String cordao="abcdefg";
System.out.println(cordao.length());
No trecho acima o método não-estático
length() da classe String é invocado. Invocar um método consiste em desviar o fluxo de controle para o código correspondente a este método, mas antes que ocorra o desvio, de alguma forma, é salvo o ponto no qual o fluxo de controle deverá retornar. O println() também é um método. A ordem de invocação é a seguinte: o compilador primeiramente gera código para invocar o método length(), após a invocação de length() é gerado o código para invocar println().

A execução do código correspondente a length()determina o número de caracteres e retorna este valor. O valor retornado por um método tem que ter um tipo. O método length() da classe String é um método do tipo int, ou seja retorna valores inteiros. Mais a frente na disciplina iremos ver como definir classes e em particular como definir métodos. Observe que os objetos correspondentes a arranjos têm um campo length enquanto que objetos correspondentes a cadeias de caracteres compartilham o método length() da classe String. A execução de um método pode receber valores através de parâmetros ou argumentos. Os parâmetros são sempre de um determinado tipo. O método length() não tem parâmetros. O parâmetro do método println() deve ser do tipo String, print() e println() serão discutidos mais a frente na disciplina. O valor retornado pelo método length() é do tipo int, o compilador cuida para que seja feita a conversão do valor de tipo int para tipo String.

O trecho de programa abaixo utiliza o método charAt(). Este método possui um parâmetro do tipo int  e retorna um valor do tipo char:
String cordao="abcdefg";
for(int i=0; i<cordao.length(); i++) System.out.print(cordao.charAt(i));
System.out.println();

O trecho acima tem efeito semelhante ao seguinte trecho:
String cordao="abcdefg";
System.out.println(cordao);

O método charAt() recebe como parâmetro um valor inteiro que deve estar entre 0 e length()-1. Se o valor do parâmetro não atender a estes limites haverá um erro em tempo de execução. O método devolve o (valor do parâmetro+1)-ésimo caractere da cadeia de caracteres. Se o argumento de charAt() for menor que 0 ou maior ou igual ao comprimento da cadeia ocorre um erro de execução: IndexOutOfBoundsException.

Construtores

 Uma classe define campos(fields), métodos(methods) e constructores( constructors). Os campos e métodos já foram parcialmente discutidos: os campos correspondem a posições de memória e os métodos são códigos que recebem 0 ou mais valores e devolvem 0 ou 1 valor. Os construtores têm similaridades com métodos (especificamente métodos de instância) mas sua função está ligada à iniciação dos objetos quando de sua instanciação. Um exemplo de similaridade entre construtores e métodos é o fato de construtores poderem receber parâmetros, mas construtores, diferentemente de métodos, não podem ter nomes arbitrários. Os nomes de  construtores são sempre o nome da classe correspondente. O trecho de programa abaixo ilustra como utilizar um arranjo de caracteres para construir uma cadeia de caracteres correspondente à seqüência do arranjo:
char[] arranjoCar;
arranjoCar= new char[]{'a', 'b', 'c ', 'd ', 'e ', 'f '};
String cordao;
cordao= new String(arranjoCar);
Este trecho corresponde ao seguinte uso de literais:
String cordao=”abcdef”;
 No trecho acima é utilizado o operador new no contexto de arranjos e no contexto de classes. A expressão
new char[]{...} aloca um bloco de memória contendo um arranjo do tipo char inicializado conforme especificado acima. A expressão new String(...) irá alocar um objeto do tipo String e retornar o endereço do bloco de memória onde foi alocado este objeto.

O operador new é que estrutura e aloca todo o bloco de memória para instâncias de arranjo e classes fazendo todas as iniciações necessárias e retornando o endereço do bloco de memória. Veremos mais a frente na disciplina que um arranjo é um tipo especial de objeto e que a alocação de um objeto e o retorno de seu endereço é feito pelo operador new. Podemos fazer quantas atribuições quisermos a uma variável do tipo referência. Quando um bloco de memória (arranjo ou objeto) previamente referido por uma ou mais variáveis fica sem “ser referido” um mecanismo da Máquina Virtual Java denominado Coletor de Lixo irá cuidar de reciclar a memória e o bloco de memória poderá ser reutilizado. O ambiente de execução da linguagem Java não exige que o programador faça a gerência dos blocos de memória.

A expressão String(arranjoCar) acima, junto ao operador new corresponde a uma chamada ou invocação de construtor (constructor), na verdade um dos vários contrutores definidos pela classe String. Somente o operador new pode invocar um construtor, ou seja, um construtor só pode ser invocado quando da instanciação de um objeto e serve para iniciar ou a inicializar este objeto. A classe String tem vários construtores. Os contrutores têm o mesmo nome da classe e só diferem entre si pelo número e tipo dos parâmetros. A classe String define um construtor com apenas um parâmetro do tipo arranjo de caractere que, por definição, aloca um objeto do tipo String onde a cadeia de caractere é formada pela concatenação dos caracteres do arranjo correspondente.

Arranjos de referências para Objetos do tipo String (Arranjos de String)

Podemos definir arranjos de String, ou sem vício de linguagem, arranjos de referências para objetos do tipo String:
String[] meses={"", "Janeiro", "Fevereiro", "Março",...,"Dezembro"};
No arranjo acima foi incluído, na posição de índice 0, um string de comprimento zero. Uma outra solução seria o uso de null.

Métodos estáticos da Classe String

Abaixo exemplificamos a utilização de um método estático da classe String. Considere que temos uma variável do tipo int contendo o dia do mês e uma variável também do tipo int contendo o número do mês.. No trecho abaixo é construido um string do dia do mês utilizando o método estático valueOf() da classe String:
int numDoDia=...;
int numDoMes=...;
String data="dia " + String.valueOf(numDoDia)+
            " de " + meses[numDoMes];
No trecho acima  invocamos o método
valueOf() da classe String. Este método cria um objeto contendo uma cadeia de caracteres correspondente à conversão do valor inteiro do parâmetro numDoDia e retorna a referência para este objeto. Descrito de outra maneira: valueOf() retorna a representação em cadeia de caracteres do argumento numDoDia.

Operador de Seleção sobre variáveis e literais do tipo String

Uma chamada de método que retorne uma referência pode ser qualificada, utilizando o operador '.', com nova chamada de método. Considere o trecho de programa abaixo:
String universidade= "Universidade Federal de Minas Gerais";
String s=universidade.substring(0,20);
A invocação do  método
substring(0,20) retorna uma referência para um objeto contendo a subcadeia "Universidade Federal". O primeiro parâmetro é o índice de início (inclusive) e o segundo parâmetro é o índice final da subcadeia (exclusivo) que é copiada da instância. Como o método substring() devolve uma referência para uma instância de String, podemos invocar qualquer método de instância utilizando esta referência:
String universidade= "Universidade Federal de Minas Gerais";
String x=universidade.substring(24,36).substring(0,5);

O trecho acima mostra duas chamadas do método substring(). A primeira chamada retorna a cadeia "Minas Gerais" e a segunda chamada retorna a cadeia "Minas".

Comparação de instâncias de String

Para comparar cadeias existem os métodos: equals(), equalsIgnorecase(), compareTo() e compareToIgnoreCase(). Se for utilizada o operador == da Linguagem Java com variáveis do tipo String a comparação será feita com as referências:
String ab="ab";
String bc="bc";
String s1=ab+"c";
String s2="a"+bc;
if(s1==s2) System.out.println("valores de s1 e s2 iguais!");
No trecho acima o conteúdo de s1 e s2 são duas referências diferentes (foi preciso alguma prestidigitação - sem trocadilhos - porque o compilador tenta reutilizar cadeias iguais!). Se quisermos comparar o conteúdo das instâncias devemos utilizar os métodos mencionados:
String ab="ab";
String bc="bc";
String s1=ab+"c";
String s2="a"+bc;
if(s1==s2) System.out.println("Endereço em s1 igual ao endereço em s2!");
if(s1.equals(s2)) System.out.println("Instancias com cadeias iguais!")
O método compareTo() é um método de instância que tem um argumento do tipo String e retorna um valor do tipo int. O valor retornado é (i) menor que zero quando a cadeia do argumento é lexicograficamente maior que a cadeia da instância, (ii) zero quando a cadeia do argumento é a mesma da instância e (iii) maior que zero quando a cadeia do argumento é menor que a cadeia da instância:
String abc="abc";
String bcd="bcd";
if(abc.compareTo(bcd)==0)
  System.out.println("cadeias iguais:"+abc);
if(abc.compareTo(bcd)<0)
  System.out.println(abc+ " precede lexicograficamente "+ bcd);
if(abc.compareTo(bcd)>0)
  System.out.println(bcd+ "precede lexicograficamente "+abc);

Os métodos equalsIgnoreCase() e compareToIgnoreCase() são similares à descrição acima mas ignoram se as letras são maiúsculas ou minúsculas.

Operações diversas sobre os caracteres e cadeias de caracteres

O método toLowerCase() da classe String retorna uma (referência para uma) instância de String na qual todos os caracteres foram convertidos para minúsculas. O método toUpperCase() retorna uma instância de String na qual todos os caracteres (letras) foram convertidos para letras maiúsculas:
String universidade= "Universidade Federal de Minas Gerais";
String uniMinusculas=universidade.toLowerCase();
String unimaiusculas=universidade.toUpperCase();

Operações involvendo a Classe Character

A linguagem Java  define a classe Character que possui vários métodos estáticos úteis no tratamento de caracteres individuais. Os seguintes métodos recebem um caractere como argumento e devolvem um valor booleano: isDigit(), isLetter(), isLetterOrDigit(), isLowerCase(), isSpaceChar(), isUpperCase().

Os comandos abaixo testam se o primeiro caractere de uma instância de String é letra ou dígito:
String s="...";
if(Character.isDigit(s.charAt(0)) | Character.isLetter(s.charAt(0))) System.out.println(...
if(Character.isLetterOrDigit(s.charAt(0))) System.out.println(...

Exercícios

  1. Escreva um trecho de programa que imprime se as letras maiúsculas são maiores ou menores, lexicograficamente, que as letras minúsculas.
  2. Escreva um trecho de programa que transforma um inteiro menor que 4000 e maior que zero em uma cadeia de caracteres correspondente a  um número romano.
  3. Escreva um trecho de programa que transforma um String em um arranjo de caracteres
  4. Escreva um trecho de programa que produz uma instância de string que seja o resultado da concatenaçao dos primeiros caracteres das palavras separadas por espaço em branco e que comecem com letra maíuscula. Exemplo: a instância "Universidade Federal de Minas Gerais"  deve resultar em "UFMG".
  5. [Aguarde o material  “Métodos Estáticos” para fazer este exercício!] As instâncias de String são imutáveis, ou seja, uma vez criadas não podem ser atualizadas. Isto pode trazer sérios problemas de desempenho. Uma alternativa ao uso da classe String é a classe StringBuffer. As instâncias de StringBuffer podem ser atualizadas (p. ex. O método de instância append(s) anexa a cadeia de caracteres referida pelo parâmetro s à instância de StringBuffer e retorna uma referência para esta instância atualizada ). As instâncias de StringBuffer alocam memória dinâmicamente, mas um construtor de StringBuffer permite que o programador expresse sua expectativa de comprimento inicial das cadeias de caracteres , o construtor StringBuffer(int i) define uma instância contendo uma cadeia de caracteres de tamanho 0 mas com espaço suficiente para acomodar cadeias de caracteres de tamanho i.  O método toString() retorna uma instância de String correspondente à cadeia de caracters da instância de StringBuffer. Reescreva o método abaixo, preservando seu comportamento, utilizando apenas a classe String.
    public static String replica(String s, int vezes){
      StringBuffer resultado = new StringBuffer(s.length()*vezes);
      for(int i=0; i<vezes; i++) {
        resultado.append(s);
      }
      return resultado.toString();
    }
  6.  

Sugestão de solucão