To get out of theory a little, the next step in the bootcamp is an exercise (which they call project challenge). I thought it would be interesting to include a practical part to consolidate the content covered so far.
What was requested was to create a project that simulates a single account in a bank, containing branch number, account number, customer name and balance. This data must come from the terminal and at the end, a message must be displayed stating that the account was created successfully and showing what data was entered. The original description can be seen here.
Well, it seems pretty simple. But one thing I really like to do in any task I receive is to break it down into very short steps, so that I have a clear path to follow. I do this also because while I'm drawing the flow in my head, I can understand if it makes sense with what was asked and I can quickly change the course of action if I think it's not working.
Therefore, the steps to follow are as follows:
It doesn't seem like something exactly complex. Starting with step 1, we have the following:
TerminalAccount.java
public class TerminalAccount { // eu sei, sou muito criativo private int branch; private String account; private String clientName; private double balance; public void createAccount(){ // aqui a mágica acontece } }
Although access modifiers have not been covered in much depth so far in the course, I chose to leave all fields private, imagining that in a banking system, branch, account, holder and balance data must be well protected and only the class itself should have access to them.
In addition, the createAccount method should not return anything, just display a message in the console, so its return is void.
This method is where some of the things that irritate me about Java begin. Instead of having a generic method that captures any type of information the user enters, the Scanner class, which is used for this, has specific methods for each primitive type. In other words, there is Scanner.nextLine() for Strings, Scanner.nextInt() for numbers, Scanner.nextBoolean for booleans...
In comparison, in C# we have a standard input method that always returns a String, which then has its type changed:
String input = Console.ReadLine(); int.TryParse(input, out number) Console.WriteLine(number) //completamente válido
Do I have to write a little more? He has. But at least I don't need to remember the syntax of all the methods, just one and convert to the type I want next. Much more intuitive.
But let's continue, there is still a long way to go to resolve this problem. The next step, then, is to ask the user to enter customer data for registration.
TerminalAccount.java
public class TerminalAccount { private int branch; private String account; private String clientName; private double balance; public void createAccount() { Scanner sc = new Scanner(System.in); System.out.print("Por favor, insira o número da agência: "); this.branch = sc.nextInt(); System.out.print("Por favor, insira o número da conta: "); this.account = sc.nextLine(); System.out.print("Por favor, insira o nome do cliente: "); this.clientName = sc.nextLine(); System.out.print("Por favor, insira o saldo inicial: "); this.balance = sc.nextDouble(); System.out.println("Olá " + this.clientName + ", obrigado por criar uma conta em nosso banco. sua agência é " + this.branch + ", conta " + this.account + " e seu saldo " + this.balance + " já está disponível para saque."); } }
Well, apparently everything is fine. Let's instantiate this class in our main method and see what happens.
Main.java
public class Main { public static void main(String[] args) { TerminalAccount account = new TerminalAccount(); account.createAccount(); } }
Oxe? Why was the account number input ignored and the algorithm already asked for the customer's name?
Reading the documentation for the Scanner class, it is explained that it breaks the input into tokens using some type of character as a delimiter, to know where to stop. In the case of the .next() and .hasNext() methods (and their variants, such as .nextInt() and .hasNextInt()), the delimiter is a global whitespace (s+) such as spaces, tabs and line breaks.
What the method does is take the token preceded by the delimiter, convert it to the specified type and return that value. The rest remains in the input buffer.
Okay, so far ok. The problem is that the .nextLine() method, used to capture Strings consumes the line break character (n), instead of discarding it. Then, when used following another that discards, it reads what was left behind and immediately ends its operation, moving on to the next line of code.
It sounds confusing, and it is. I made a diagram to help you understand the madness that this flow is:
Tá, e como consertamos essa lambança? Simples: adicionando um novo Scanner.nextLine() logo depois do .nextInt() para "limpar" o que sobrou. É bonito? Não, mas resolve.
Dá pra mudar o delimitador para aceitar a quebra de linha (\n ) em vez de um whitespace comum (\s+), mas isso poderia quebrar a forma com que as informações são quebradas em tokens, então é melhor deixar pra lá.
TerminalAccount.java
public class TerminalAccount { private int branch; private String account; private String clientName; private double balance; public void createAccount() { Scanner sc = new Scanner(System.in); System.out.print("Por favor, insira o número da agência: "); this.branch = sc.nextInt(); sc.nextLine(); //... } }
Mais feio que bater na mãe.
Beleza, funcionou. Poderíamos dizer que o exercício está completo, mas vamos por um segundo imaginar que o usuário, sem querer, digitou uma letra na hora de colocar o número da agência:
Eita lasqueira.
Isso acontece porque, como já sabemos, a tipagem do Java é estática e o Scanner está esperando um número mas quando colocamos uma letra junto com um número, essa cadeia se torna uma String. O input que estava esperando um int recebeu uma String e ficou confuso tal qual uma criança que recebe meias de presente de natal.
Para remediar essa situação, podemos criar um loop simples, que informe para o usuário que a informação que ele inseriu está incorreta de acordo com as especificações do sistema e pedir que ele insira os dados novamente (de maneira correta, dessa vez). Como não sabemos quantas tentativas o usuário vai levar para inserir os dados corretamente, um while parece adequado.
public class TerminalAccount { private int branch; private String account; private String clientName; private double balance; public void createAccount() { Scanner sc = new Scanner(System.in); boolean isBranchNumberInputCorrect = false; do { try { System.out.print("Por favor, insira o número da agência: "); this.branch = sc.nextInt(); sc.nextLine(); isBranchNumberInputCorrect = true; } catch (InputMismatchException e) { System.out.println("Por favor, insira apenas números inteiros para o número da agência."); sc.nextLine(); } } while (!isBranchNumberInputCorrect); //... } }
Aqui criamos uma variável de controle chamada IsBranchNumberInputCorrect (porque, novamente, sou muito criativo quando se trata de nomes), inicializada em false. Em seguida, começamos o bloco do, uma vez que queremos que o código faça uma ação antes de verificar se o dado inserido é valido ou não e jogamos nosso input lá pra dentro.
Caso dê tudo certo, o dado inserido será armazenado no campo branch, qualquer caractere sobrando será consumido pelo Scanner.nextLine() e a nossa variável de controle será atualizada para true. Aí a condição do while vai checar se isBranchNumberInputCorrect é false. Se for, reinicia o loop no caso de sucesso, o laço é encerrado.
Agora, caso o usuário insira algo não esperado (como uma String), o método Scanner.nextInt() vai emitir um evento de erro InputMismatchException, que será capturado pelo nosso bloco catch. Uma vez lá dentro, o código vai exibir uma mensagem de erro alertando que o tipo de dado está errado e consumido qualquer caractere que tenha ficado pra trás.
A gente pode fazer a mesma coisa com o input de saldo, para garantir que o valor inserido sempre será numérico e não permitir que a aplicação quebre caso seja inserido algo como 12,56f:
public class TerminalAccount { private int branch; private String account; private String clientName; private double balance; public void createAccount() { Scanner sc = new Scanner(System.in); //... boolean isBalanceInputCorrect = false; do { try { System.out.print("Por favor, insira o saldo inicial: "); this.balance = sc.nextDouble(); sc.nextLine(); isBalanceInputCorrect = true; } catch (InputMismatchException e) { System.out.println("Por favor, insira apenas valores decimais."); sc.nextLine(); } } while (!isBalanceInputCorrect); //... } }
Poderíamos parar por aqui e dar esse exercício como encerrado, mas ainda tem um bug que requer um pouco de atenção. O que aconteceria se, em vez de delimitarmos nosso saldo com uma vírgula (,) usássemos um ponto (por exemplo, 10.56)?
Isso acontece devido ao locale, a adaptação do input à cultura do local. Aqui no Brasil, o decimal é delimitado pela vírgula, então o método não entende que essa separação com ponto é válida.
A documentação da classe Scanner nos mostra que é possível alterar a cultura para uma que atenda ou um ou outro padrão, mas não os dois ao mesmo tempo. Também é possível alterar especificamente o delimitador, para um símbolo ou para outro, mas não os dois ao mesmo tempo.
Um dos métodos para solucionar esse problema não é muito elegante, mas resolve: em vez de capturar o dado diretamente como double, vamos usar o método Scanner.nextLine() para pegar o input como uma String, trocar os pontos por vírgula e tentar trocar o tipo para double.
public class TerminalAccount { private int branch; private String account; private String clientName; private double balance; public void createAccount() { Scanner sc = new Scanner(System.in); //... boolean isBalanceInputCorrect = false; do { try { System.out.print("Por favor, insira o saldo inicial: "); String balanceString = sc.nextLine().replace(",", "."); this.balance = Double.parseDouble(balanceString); isBalanceInputCorrect = true; } catch (NumberFormatException e) { System.out.println("Por favor, insira apenas valores decimais."); } } while (!isBalanceInputCorrect); //... } }
Além da alteração do método de captura do dado, tivemos mais algumas modificações: retiramos as chamadas para o método Scanner.nextLine() que serviam apenas para consumir os caracteres remanescentes, porque nosso input já faz isso pra gente. Além disso, o tipo do erro mudou: agora não se trata de um erro de incompatibilidade de tipo (InputMismatchException), mas sim um de erro no formato do número (NumberFormatException).
Com essas alterações feitas, bora ver se tudo deu certo:
Deu tudo certo! Com isso, conseguimos dizer que o exercício está concluído (finalmente)!
O repositório desse exercício está disponível aqui caso tenha interesse de ver.
E é isso. Até o próximo módulo!
The above is the detailed content of Exercise - Simulating a Bank Account Using the Terminal. For more information, please follow other related articles on the PHP Chinese website!