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
.
-
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. ↩
-
Essa optimização não será notada nos sistemas actuais, mas é, mesmo assim, uma prática que deve ser sempre seguida. ↩
-
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. ↩
-
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()
. ↩ -
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. ↩