Lendo e processando informações do teclado usando Java

Lendo e processando informações do teclado usando Java
Imagem de destaque #cover

Fomos contratados para desenvolver um software em Java para uma empresa e a instalação do nosso programa é feita via console.

Para conseguir usufruir do nosso produto o usuário precisa ler e aceitar os termos de uso, caso contrário não podemos permitir que a instalação continue.

Além disso, para estatísticas internas da empresa, após aceitar os termos o usuário também deve nos informar seu nome completo.

Banner da promoção da black friday, com os dizeres: A Black Friday Alura está chegando. Faça parte da Lista VIP, receba o maior desconto do ano em primeira mão e garanta bônus exclusivos. Quero ser VIP

Vamos então começar criando a funcionalidade do usuário poder aceitar ou não os termos de uso do nosso produto.

Após a criação, testamos e obtivemos o seguinte resultado:

Eita, como assim? O programa não nos deixou responder, simplesmente considerou que aceitamos os termos de uso e finalizou a instalação. E agora?

Precisamos que o programa, ao chegar na pergunta "Você concorda com os termos?", espere por uma resposta do usuário e a processe de forma que, caso seja ‘S’ termine de instalar o programa, caso seja ‘N’ interrompa a instalação. Como fazer isso com Java?

Classe Scanner

Antigamente, fazer essa operação de ler informações era bem complexa. Vendo o quão recorrente essa funcionalidade era requisitada pelos desenvolvedores foi criada a classe Scanner, que tem como objetivo ler informações dentro do Java de forma mais fácil.

"Beleza cara, entendi, agora me mostre o código!"

Lendo entrada do teclado com a classe Scanner

Para usar a classe Scanner o primeiro passo é instância-la:


mensagemDeTermos();
new Scanner();
completaInstalacao();

Note que a instanciamos logo após o método que chama a mensagem de termos de uso do nosso software e antes do método que completa a instalação, pois precisamos que o usuário primeiro leia os termos antes de aceitá-los.

Ao fazer isso recebemos um erro de compilação.

O Scanner precisa saber de onde virão as informações, mas em nenhum momento informamos isso. No nosso caso precisamos que o usuário digite as entradas para que sejam lidas, ou seja, as informações virão do teclado.

Para informar que usaremos o teclado, vamos adicionar o parâmetro System.in. Ficando com o seguinte resultado:


new Scanner(System.in);

Agora que temos a instância, vamos atribuí-la para um objeto chamado leitor, pois é boa prática atribuirmos nomes que fazem referência ao objetivo do nosso objeto. Nosso Scanner nada mais é do que um leitor de entradas.


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
completaInstalacao();

Agora que temos nosso leitor, qual o próximo passo? Ler as entradas, certo? Para isso usaremos o método [next()](https://docs.oracle.com/javase/7/docs/api/java/util/Scanner.html#next()). Este método lê a próxima entrada do usuário.


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
leitor.next();
completaInstalacao();

Pronto, agora nosso leitor foi atribuído a um objeto e usamos o método next() para pedir ao usuário que ele digite uma resposta. Vamos testar:

Veja que ele espera a nossa resposta antes de finalizar a instalação. Dando continuidade ao teste, vamos aceitar os termos:

Tudo funcionando, nosso programa aguardou a resposta do usuário e instalou o software, dada a resposta positiva.

Mas e se fosse "não"? Isto é, e se o usuário não concordasse com os nossos termos? O que aconteceria? Vamos testar e conferir:

O resultado esperado não era esse, não é mesmo? Para qualquer que seja a resposta do usuário, nosso software está finalizando a instalação...

Se pararmos para pensar, estamos apenas recebendo a resposta, não estamos processando-a. Em outras palavras, não estamos verificando se a resposta é ‘S’ ou ‘N’, logo, independente da resposta o método completaInstalacao() é chamado.

Para resolvermos essa situação vamos primeiro atribuir a resposta do nosso usuário a uma variável para não perder a entrada lida pelo nosso leitor:


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
String resposta = leitor.next();
completaInstalacao();

Hmm… já está melhor, mas isso ainda não resolveu nosso problema, ainda temos que processar a informação recebida, pois nosso programa não é mágico, precisamos mostrar pra ele qual direção seguir, ou seja, se ele continua a instalação ou não.

Para verificar a resposta, vamos criar uma condição que vai comparar se a resposta é ‘S’ para que seja concluída a instalação. Como queremos comparar dois objetos, podemos usar o método equals, que faz exatamente isso:


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
String resposta = leitor.next();
if("S".equals(resposta)){
    completaInstalacao();
}

Ao olhar o código, pode não parecer natural chamar o método equals a partir de uma string hard coded, mas fazer isso é uma boa prática, pois temos certeza que a string ‘S’ não é nula, logo nos prevenimos contra NullPointerException.

Pronto, vamos testar e ver se tudo está funcionando:

Com a resposta sendo positiva nosso programa funciona. Isso já acontecia antes de alterarmos o código, mas e se recebêssemos uma resposta negativa? Ou seja, o que aconteceria se o usuário não concordasse com os termos? Vamos ver:

Nada acontece! Nosso software termina a execução e o usuário fica sem entender absolutamente nada. O problema, no caso, foi que só ensinamos o nosso software a processar o ‘S’, ou seja, precisamos fazer o mesmo caso a resposta seja ‘N’.

Então vamos criar outra condição para caso a resposta seja negativa:


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
String resposta = leitor.next();
if("S".equals(resposta)){
    completaInstalacao();
} else if("N".equals(resposta)){
    cancelaInstalacao();
}

Pronto, vamos testar para validar o novo comportamento que criamos:

Isso ai! Mais uma funcionalidade feita. Mas e se o usuário responder algo diferente de ‘S’ ou ‘N’? Por exemplo responder usando "s",”n”,”sim”,”não” ou algo assim, o que acontece? Vamos ver:

Vish, ele nem sequer enviou uma resposta, imagine a cara do usuário ao ver isso? Bem confusa, né?

Precisamos enviar uma resposta caso a entrada seja diferente de ‘S’ ou ‘N’, então adicionaremos mais uma condição no nosso código. Podemos resolver essa situação da seguinte maneira:


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
String resposta = leitor.next();

if("S".equals(resposta)){
    completaInstalacao();
} else if("N".equals(resposta)){
    cancelaInstalacao();
}else{
    System.out.println("Resposta inválida");
}

Pronto, agora teremos uma resposta para qualquer entrada que o usuário digitar. Vamos ver se isso realmente funciona:

Boa, funcionou! Uhul, mas nosso código ficou meio feio, né? O ideal seria jogar nossa lógica de validação (os if's) dentro de um método e apenas chamá-lo, inclusive é uma boa prática fazer isso.

Para fazer essa refatoração, primeiro criaremos um método e jogaremos toda nossa lógica de validação de resposta dentro dele:


public static void validaResposta(String resposta) {
    if ("S".equals(resposta)) {
        completaInstalacao();
    } else if ("N".equals(resposta)) {
        cancelaInstalacao();
    } else {
        System.out.println("Resposta inválida");
    }
}

Agora vamos apenas chamar esse método:


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
String resposta = leitor.nextLine();
validaResposta(resposta);

Caso queira saber mais sobre refatoração, código limpo e etc, deixarei o link de um artigo que trata exatamente sobre esse assunto, vale a pena dar uma olhada.

Agora que já conseguimos implementar a primeira feature que consistia em receber uma resposta do usuário para aceitação ou não dos termos de uso, vamos para a segunda parte, no caso, implementar uma função para receber o nome do nosso usuário.

Começaremos pedindo ao usuário para nos fornecer seu nome, mas antes de implementar tal funcionalidade, vamos pensar: Onde devemos escrever esse código?

Faz sentido pedir o nome do usuário que digitou ‘N’ ou o usuário que digitou uma resposta inválida? Não, né? Logo, nosso programa só deve perguntar o nome para os usuários que digitaram ‘S’, portanto, devemos codificar essa nova funcionalidade dentro do bloco if onde a condição verifica se a resposta é igual a 'S'.

Agora sim podemos implementar esse código. Como estamos fora do escopo do nosso leitor teremos que instancia-lo novamente, já que só o usaremos uma vez podemos instanciar a classe Scanner e já chamar o método que queremos, no caso o next(), e já salvar em uma variável. Por enquanto nada novo né?

Logo, dentro de validaResposta(), teremos o seguinte código:


public static void validaResposta(String resposta) {
    if ("S".equals(resposta)) {
        completaInstalacao();
        solicitaNome();
        String nome = new Scanner(System.in).next();
    }
    //resto do código
}

Repare no nosso resultado:

Funcionou, conseguimos tanto aceitar os termos quanto informar nosso nome.

Dias depois, nosso chefe veio reclamar de um bug no software. Aparentemente só estava sendo salvo a primeira palavra do nome e não o nome completo.

Para testar vamos imprimir a variável nome (não esqueça de remover essa linha de código após os testes) e ver o que realmente está sendo armazenado.


public static void validaResposta(String resposta) {
    if ("S".equals(resposta)) {
        completaInstalacao();
        solicitaNome();
        String nome = new Scanner(System.in).next();
        System.out.println("Nome digitado: " + nome);
    }
    //resto do código
}

Agora só falta testar:

Eita, o bug é real! O nome digitado foi "Mario Alvial", mas nosso programa só considerou a primeira palavra, quando, na verdade, queríamos que ele lesse todas as palavras que o usuário digitasse.

Se olharmos a documentação da classe Scanner, o método next() lê apenas, e somente apenas, a próxima entrada, por isso, se o usuário tiver um nome composto, não vamos conseguir o resultado esperado.

Para resolver tal problema podemos usar outro cara da classe Scanner.

O método nextLine

Precisamos de um cara que leia a linha inteira e esse cara é o método [nextLine()](https://docs.oracle.com/javase/7/docs/api/java/util/Scanner.html#nextLine()). Sendo bem técnico, ele pula a linha que está e lê tudo o que foi pulado. Vamos lá!


public static void validaResposta(String resposta) {
    if ("S".equals(resposta)) {
        completaInstalacao();
        solicitaNome();
        String nome = new Scanner(System.in).nextLine();
        System.out.println("Nome digitado: “ + nome);
    }
    //resto do código
}

Agora nos resta testar:

Opa calma aí, nosso programa passou batido pela pergunta, o que aconteceu? Ao invés de consertarmos o bug só o pioramos.

Após ler novamente a documentação (javadoc sempre salvando) consegui entender o problema. Vamos destrinchar esse erro.

O método next() é o culpado da situação, como já foi falado ele lê a próxima entrada e só. Nessa linha, onde o usuário aceita os termos, temos duas entradas a ser lidas.

"Tá doido? Só tem uma, ou é ‘S’ ou é ‘N’”

Concordo, só tem uma entrada visível a ser lida, mas após a resposta do usuário temos uma quebra de linha certo? Essa quebra de linha é representada por \n, que nada mais é do que uma sequência de escape para o compilador, isto é, quando o compilador vê o \n ele pula para a próxima linha.

Então, esse \n não foi lido pelo next() e ficou para ser lido pelo nextLine() que vem em seguida, logo o nextLine() lê o resto da linha vazia e termina a execução do nosso programa, ele nem sequer chega a ler o nome.

Para exemplificar melhor vou mostrar com um desenho de qualidade:

Para resolver isso precisamos fazer com que o resto da linha onde o usuário aceita ou não os termos seja lida antes de chamarmos o nextLine() para receber o nome.

Podemos abordar essa situação de duas formas:

Adicionando um nextLine() logo após o next() para que após a primeira resposta do usuário o nosso leitor já seja posicionado na linha seguinte, deixando nosso código desse jeito:


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
String resposta = leitor.next();
leitor.nextLine();
validaResposta(resposta);

Vamos testar e ver se realmente essa abordagem funciona:

Boa, agora sim, funcionou perfeitamente.

Outra abordagem seria eliminar o mal pela raiz, substituindo o nosso método next() pelo nextLine(), ficando assim:


mensagemDeTermos();
Scanner leitor = new Scanner(System.in);
String resposta = leitor.nextLine();
validaResposta(resposta);

Novamente, vamos testar e ver se conseguimos o resultado esperado:

Tudo certo aqui também. Nos dois casos, temos o mesmo resultado, mas a primeira abordagem não é recomendada, pois estamos usando um método a mais sem necessidade. Com a segunda abordagem nosso método fica mais limpo.

Parabéns!!! Conseguimos criar todas as funcionalidades pedidas e "blindamos" nossa aplicação contra cenários problemáticos que podem surgir durante o uso do nosso software.

Eai, já teve problemas em receber entradas dos seus usuários? Compartilhe sua experiência aqui nos comentários.

Expandindo seu conhecimento sobre a classe Scanner

Além de usarmos a classe Scanner para ler entradas do teclado, podemos usar também para ler arquivos externos. Para saber mais a respeito acesse esse post onde é mostrado como ler e processar arquivos externos usando a classe Scanner.

Aprenda outras funcionalidades bem úteis para o seu dia a dia na Formação Java, onde você vai aprender técnicas tanto básicas quanto avançadas para criar suas aplicações.

Veja outros artigos sobre Programação