Ir para o conteúdo

Ler a partir do Teclado

Apesar de cada vez mais as aplicações serem orientadas para interacções que impliquem o uso de interfaces gráficas, é muito importante o conhecimento de como ler eficientemente dados directamente do teclado, aquando do desenvolvimento de aplicações que usem apenas a consola.

Ler dados da consola, e neste caso do standard input, tem tanto de simples como de complicado :), embora o processo seja simples, nem sempre a formas mais directas nos permitem os resultados desejados. Coisas como detectar introdução de dígitos por parte do utilizador, ou até conversão de caracteres1, podem muitas vezes complicar a estrutura do nosso programa.

No decorrer do tutorial serão apresentadas duas formas genéricas de ler dados, uma usando as streams típicas, outra usando um objecto especifico para a leitura de dados da consola. Nos dois casos as soluções apresentadas poderão ser diferentes de outras soluções já vistas pelo leitor, é necessário ter em consideração que as soluções são apenas algumas das possíveis soluções a implementar.

Ao longo do tutorial iremos falar de streams, buffers, leitura e escrita, sem darmos muitas explicações sobre o que são efectivamente streams e o que significam todos estes termos, aconselha-se a leitura do capítulo Leitura e Escrita, Streams do tutorial de Java caso estes termos sejam desconhecidos.

Streams Standard

Virtualmente todos os sistemas operativos oferecem 3 streams padrão, uma de leitura, mapeada para o teclado, uma de escrita, mapeada para o ecrã, e uma de erros, que também se encontra mapeada para o ecrã. Naturalmente, estes mapeamentos podem ser alterados, e o sistema operativo onde a nossa aplicação esteja a ser executada pode ter algum, ou todos estas streams mapeadas para outros dispositivos. No entanto, vamos assumir que isso não acontece, e que a stream de entrada é o teclado, e a de saída e de erro é o nosso ecrã.

Assim, para acedermos a cada uma destas streams teremos de usar os objectos que o Java nos fornece em System.in, System.out e System.err para leitura, escrita e erros, respectivamente.

Reparem que isto são atributos estáticos da classe System, e embora sejam usados sem grande cuidado ou atenção a esse facto, a verdade é que são objectos, tal como qualquer outro objecto que seja instanciado por nós, a diferença principal está no facto de serem instanciados pela plataforma Java, sem que o programador tenha de se preocupar com a sua gestão.

Existem várias classes de leitura de streams, essas classes podem dividir-se entre classes para streams de texto, streams binárias, com buffer ou sem buffer, para o nosso tutorial vamos apenas focar as streams de texto e, embora a leitura de dados do teclados não seja, tipicamente, um processo intensivo a nível de consumo de recursos, vamos usar buffers para optimizar a nossa leitura. Vamos assim, focar o uso de BufferedReader e InputStreamReader.

InputStreamReader é uma classe que nos permite transformar a stream System.in numa stream de texto:

InputStreamReader iReader = new InputStreamReader(System.in));

O que estamos a fazer é criar uma stream de texto com base numa stream binária definida pelo objecto System.in, a partir deste momento, a conversão entre texto e binário é completamente feita pela classe InputStreamReader, e não teremos de fazer conversões manuais.

Mas como falamos, vamos querer usar um buffer, para tornar a leitura algo mais optimizado2:

InputStreamReader iReader = new InputStreamReader(System.in));
BufferedReader bReader = new BufferedReader(iReader);

Podemos condensar o código acima, e formar uma linha que é comum ser vista quando se trabalha com streams3:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

Como apenas precisamos de um objecto para aceder à stream, e o tipo de objecto define os métodos que temos disponíveis, não nos é útil manter uma referência para a instância de InputStreamReader, dessa forma podemos passar logo para o construtor a nova instância. Embora o código possa parecer mais complicado de ler, é algo simples de perceber se invertermos a ordem e lermos do fim para o início: "Coloca o System.in dentro de uma nova instância de InputStreamReader; pega nessa nova instância e coloca-a dentro de uma nova instância de BufferedReader".

O resultado é um buffer de caracteres, que usar uma stream de caracteres como base, que por sua vez, usa uma stream binária para se apoiar. Todas as conversões necessárias são feitas automaticamente e ganhamos métodos mais úteis que apenas leitura de bytes ou de caracteres, um a um.

Para fazermos algo útil com o código acima teremos de criar mais algum código, neste caso, vamos pedir o nome do utilizador e responder com um simpático "Olá".

public void hello() {
    System.out.println("Como se chama?");
    try {
        String name = reader.readLine();
        System.out.println("nOlá, " + name);
    } catch (IOException ex) {//Neste exemplo apanhamos a excepção aqui...
        System.err.println("Ocorreu um erro ao ler o texto, por favor troque de nome!");
        System.err.println("Motivo:" + ex.getMessage());
        //Não fazemos nada de especial se ocorrer um erro, mas apenas porque é um exemplo simples
        //iremos ver outras situações onde tratamos melhor os erros.
    }
}

Com este pequeno método podemos já ver como é simples ler dados de um teclado4.

Esta forma de ler, apesar de simples, tem como limitação o facto de apenas conseguirmos ler linhas completas, terminadas por \n, o terminador de linha de sistema unix, \r, o elemento que indica passagem do cursor para o início da linha, e \r\n o conjunto que permite mudar de linha em sistemas Windows. Assim, a única forma que possuímos de ler dados, é pedir ao utilizador que introduza os valores e pressione em Enter.

Obtemos de retorno uma string, que contém o texto introduzido, excluindo qualquer terminador que tenha sido usado, e que podemos transformar posteriormente. Não temos forma de limitar os dados introduzidos, nem de os validar, e todos os valores serão aceites, mesmo que estejamos à espera de valores numéricos, os utilizadores podem sempre introduzir texto.

O uso de streams deste modo força-nos a criar mais código de validação de dados, podendo ser mais completo, no entanto, para leituras simples, ou que não necessitem de muito controlo sobre os dados enquanto estão a ser introduzidos, esta é uma excelente forma de ler dados.

A classe Scanner

A classe Scanner trata os dados através de tokens, dividindo o que o utilizador introduzir e permitindo escolher o que pretendemos ler

A classe Scanner tem como token predefinido o espaço em branco, mas isso não nos impede de alterar usando o método useDelimiter() que tanto recebe um pattern como uma string que seria usada como pattern. Como o useDelimiter() devolve a própria instância de Scanner pode-se fazer um código legível assim:

Scanner s = new Scanner(input).useDelimiter("\n---\n");

E assim manter um código que funciona e, ao mesmo tempo, mais fácil de ler porque sabemos à partida o delimitador daquele Scanner. O pattern usa a sintaxe do regex (regular expressions) 5 para a separação em tokens do que lê. Isto é só aplicável para o método next(). O Scanner contem vários métodos que usam o next(), mas trabalham com código regex pré-programados. Isto inclui: nextLine(), nextShort(), nextInt(), ..., o que torna a classe mais útil das incluídas com o Java para leitura do que é enviado para o stdin (standard input: Um apontador para o espaço de memória que reflete, por definição, o que é escrito no teclado embora possa ser redirecionado para outra fonte), ou para leitura de ficheiros (juntamente com as classes que já foram faladas que o fazem).

A classe Console

Uma alternativa à manipulação de streams será o uso da classe Console.


  1. Devido a algumas razões históricas, as streams padrão de leitura, escrita e erro, são streams binárias e não streams de texto, como se poderia esperar. 

  2. Essa optimização não será notada nos sistemas actuais, mas é, mesmo assim, uma prática que deve ser sempre seguida. 

  3. Curiosamente, todas as relacionadas podem ser construídas de forma similar, é possível criar um buffer com uma outra stream que por sua vez usa como base uma stream binária. 

  4. Ignoramos a criação de streams de escrita com buffer, vamos apenas concentrar a nossa atenção em streams de leitura, nos exemplos seguintes usamos sempre o método System.out.println()

  5. Regex é quase uma linguagem de programação por si só e permite a pesquisa de informação de uma maneira muito detalhada. Para mais info: http://pt.wikipedia.org/wiki/Regex. Não muito completo, mas dá para dar noções.