Java 101

A sintaxe do Java

A sintaxe do Java


A intenção desse material é te apresentar o básico sobre Java oferecendo ferramentas para que você possa aprender a programar. Nenhum material por sí só fará o trabalho completo, para que você possa se tornar um desenvolvedor é preciso muito exercício, por isso vá lentamente avançando sobre os tópicos propostos e fazendo todos os exercícios propostos.

Nesse sessão você vai aprender:

  • O que é uma linguagem de programação e no que ela difere de uma linguagem natural

  • Os elementos básicos da sintaxe da linguagem Java


Anteriormente aprendemos o que é Java, porque precisamos de uma linguagem de programação e como criar nosso primeiro programa Java. Agora vamos aprender um pouco mais a sintaxe Java e sobre um assunto muito importante para qualquer desenvolvedor: algoritmos. Qualquer linguagem de programação tem uma sintaxe e você tem que respeitar ela por um motivo meio óbvio e muito importante: o computador é extremamente burro.

Linguagens Formais e Linguagem Natural

Antes de entender porque o computador é extremamente burro, vamos tentar diferenciar uma linguagem de programação das linguagens que usamos para conversar com outras pessoas. Se você nunca estudou formalmente o que é uma linguagem de programação, talvez seja preciso definir corretamente o que a difere de outras linguagens.

No dia a dia, nós somos acostumados a um tipo de linguagem que é extremamente maleável e pode ser compreendida mesmo que esteja formalmente errada. Eu posso omitir um objeto, inverter sujeito e predicado (todo mundo entendia o Mestre Yoda) e a comunicação continua acontecendo normalmente. Na nossa cultura, infelizmente, são raras as pessoas que amam estudar a estrutura de português, eu mesmo não sou uma delas apesar de gostar de leitura. Conhecer mais a língua que falamos não é comum porque podemos ser entendidos facilmente mesmo usando estruturas básicas e é uma atividade hercúlea e extremamente chata. O português por ser uma língua falada em locais bem diferentes é cheio de regras e excussões de difícil entendimento, a nossa língua é falada no Brasil 🇧🇷, Angola 🇦🇴, Cabo Verde 🇨🇻, Guiné-Bissau 🇬🇼, Guiné Equatorial 🇬🇶, Moçambique 🇲🇿, São Tomé e Príncipe 🇸🇹, Timor-Leste 🇹🇱, China 🇨🇳 e até em Portugal 🇵🇹. Esse texto mesmo, apesar de parecer correto, se passar por uma revisão profissional será alvo de várias correções sutis que faço porque são comuns na coloquialidade mas podem casuar pequenos desentendimentos na língua escrita, o mais comum deles é a troca de pessoa ao me referir a você leitor.

As línguas Português, Inglês, Mandarim, Japonês e até o Javanês são o que conhecemos como linguagens naturais, pois elas emergem da experiência humana e são compreendidas por humanos. Computadores não entendem essas linguagens, eles podem apenas capturar símbolos, mas eles não conseguem compreender.

— Ah, mas tem o GPT-3 que consegue ler e escrever bons textos.

Calma lá! É preciso entender como funciona um computador para não cair no jornalismo barato e marketing agressivo de companhias de Inteligência Artificial. GPT-3 não é aquilo que foi prometido e tem suas limitações. Eu recomendo ler o artigo "GPT-3, Falsário: o gerador de linguagem do OpenAI não tem ideia do que está falando" (se não lê inglês, use o Google Translator). O entendimento do que é uma linguagem de programação é muito importante, por isso vamos fazer uma analogia para demonstrar como lidar com a gramática delas.

Imagine que um computador é como um falante de português que está preso dentro de um quarto com um livro de regras. Sua função é, ao receber caixas com texto em chinês, deve consultar o livro de regras para identificar os símbolos no texto e formular uma resposta baseado nas regras e no texto recebido e enviar a resposta para fora do quarto. Esse homem não sabe chinês e nem consegue compreender o que ele está respondendo, ele só está seguindo as regras de quem escreveu o livro. O homem compreende a comunicação? Ao interlocutor fora do quarto, parece que o homem fala chinês?

o quarto chines
Figura 1. O Quarto Chinês

Programas são o livro de regras que é escrito por programadores. Isso significa que, por melhor que seja um programa, um computador não tem compreensão do que está acontecendo. O interlocutor até poderá acreditar que o programa entende chinês, mas se houver alguma situação não prevista no livro de regras, não será possível formular uma resposta e o interlocutor ficará em dúvida.

Como já falamos, um computador é uma entidade extremamente burra. Ela só vai conseguir compreender as regras se elas forem muito bem escritas em uma linguagem muito bem estruturada. Essa linguagem vai ter uma sintaxe bem definida e se por algum motivo ela for violada o computador não será capaz de compreender. Ou seja, o computador não pode usar daquilo que todos os humanos tem: bom senso. Ele não vai conseguir compreender se um sujeito for omitido. É por esse motivo que as chamamos de Linguagens Formais.

Linguagens Formais
São linguagens que podem ser representadas de maneira finita e precisa através de sistemas com sustentação matemática (dispositivos formais ou modelos matemáticos).

Java, C, Javascript, PHP, Python, etc…​ são linguagens formais. Elas não emergem da experiência humana, mas são propostas por humanos para se comunicar com computadores. Um humano consegue identificar um erro em uma linguagem natural e mesmo assim compreender o que é proposto, mas um erro em uma linguagem formal impossibilita todo o processo. Se você quiser saber como definir uma linguagem formal, eu já escrevi sobre isso em "Como criar uma linguagem usando ANTLR4 e Java".

A Sintaxe Java

Agora vamos falar do Java…​ Java é uma linguagem que normalmente chamamos de C-Like, isso significa que ela herda muitas características do C. Se você nunca ouviu falar de C, não se preocupe, apesar dela ser uma das linguagens mais influente da história, ela não tem muito espaço no desenvolvimento web moderno, está nichada em desenvolvimento embarcado e nos drivers e kernel dos sistemas operacionais. Mas o C emprestou ao Java muito das estruturas que usamos no dia a dia e são nessas estruturas que vamos focar por enquanto.

O C é uma linguagem de propósito geral e estruturada. Isso significa que é possível escrever qualquer tipo de programa com ela, mas por suas características o estilo de programação mais comum é o imperativa. Quando falamos de paradigma imperativa dizemos que nosso programa está definindo a forma como as coisas devem ser feitas e não a definição formal da solução, como acontece com a programação declarativa. A programação declarativa está focada na transformação do dado, enquanto a programação imperativa irá ditar os passos que devemos fazer para transformar os dados.

Com o Java é possível programar das duas formas, mas como vamos estudar a sintaxe da linguagem vamos nos preocupar por enquanto apenas com a programação imperativa, por isso vamos deixar orientação a objetos e programação funcional para outro momento. Pensar no Java como uma linguagem imperativa é pensar que devemos escrever um programa que irá transformar os dados de acordo com os passos que definimos, então precisamos pensar em como esses dados serão transformados. Esse "como" é o que chamamos de algoritmo. Algoritmo é uma receita de bolo muito bem definida que transforma dados. Por "muito bem definida" entenda que ele deve ter uma entrada, uma saída e passos definidos, os passos serão definidos através da sintaxe.

Já falei que um computador é algo extremamente burro? Sim! Tudo que ele faz é ler um programa, executar uma instrução e executar a próxima instrução. Cada instrução altera o estado interno da aplicação, esse estado por sua vez é a memória do computador. Quando falamos de programação imperativa, como o fluxo da aplicação define os processos de transformação do dado a execução pode ter caminhos diferente dependendo dos dados. Em muitos casos o processo pode ser visualizado através de fluxogramas simples. Sempre que você for tentar entender um programa estrutural, você vai ter que ter em mente quais são os dados relevantes na execução e o fluxo da execução.

fluxograma
Figura 2. Fluxograma simples definindo o processo de leitura de um arquivo.

As estruturas que vamos falar são usadas para definir esse fluxo, como em todas linguagens C-like elas tem nomes em inglês mas elas refletem as decisões que devem ser feitas baseadas nos dados em memória. Essas estruturas são validadas em temos de compilação, mas se você usa uma boa IDE você vai ver se houver um erro durante a edição do seu código fonte. Enquanto essas estruturas não estiverem muito bem definidas, o programa não poderá ser compilado e por isso não poderá ser executado.

Para facilitar o entendimento do fluxo, abaixo listo todas as estruturas que vamos detalhar resumidamente com uma tradução livre to termo em português. Ao lado de cada uma temos a documentação oficial (para versão 8 do Java) com a especificação formal. Não se preocupe se você não conseguir entender a documentação. Eu fiz a tradução para que você possa compreender melhor, nunca a utilize porque isso não é comum, a não ser que você deseje aprender Potigol, a tradução serve para você ver que tem uma lógica na nomenclatura, é como se o código fosse um tipo de linguagem verbalizável.

1. Bloco (Block)

Um bloco de código é uma estrutura que pode ser tanto obrigatória quanto opcional. Essa estrutura é definida {} e dentro desse bloco teremos um novo escopo de variáveis assim como as instruções que vão definir esse bloco. Por escopo entenda que toda variável definida dentro de um bloco será conhecida apenas por aquele bloco e todo bloco definido dentro dele. Vamos ver a definição de variável no próximo tópico.

Observe o código abaixo. Nele temos os blocos B1 a B4. Os blocos B1 e B2 fazem parte de estruturas mais complexas e são obrigatórios, que no caso são uma classe e um método respectivamente (não vamos falar da definição de classe e método por enquanto). Já os blocos B3 e B4 são opcionais e estão aí para mostrar que podemos criar um bloco quando bem entendermos, apesar dessa não ser uma prática comum no desenvolvimento Java. 🤓

public class HelloWorldSintaxe { // B1
    public static void main(String[] args) { // B2
        System.out.println("Olá mundo");

        String variavel = "abc";
        System.out.println("Valor de variavel=" + variavel);

        {} // B3: Bloco vazio

        { // B4
            String variavel2 = "xyz";
            System.out.println("Valor de variavel2=" + variavel2);
        }

        // System.out.println("Valor de variavel2=" + variavel2);  // Se você
    }

    // private void x() return 1; // Bloco é obrigatório no caso de método, essa construção vai falhar
}

Se você começar a brincar com esse código, vai ver que a variavel2 só pode ser usada dentro do B4. Isso é o que chamamos de escopo, ao finalizar a execução de B4 ela é completamente desnecessária e poderá ser eliminada da memória.

2. Declaração (Statement)

Se você pegar um código Java, ou de qualquer outra linguagem C-Like, vai perceber que o comportamento dele é sempre similar. Existe um método/função main que deve ter uma assinatura especifica e uma série de declarações.

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

No código acima, temos o mais simples Hello World escrito em Java. Quando a JVM vai executar esse código será feito como é feito em qualquer outra linguagem imperativa, primeiro a JVM lê a primeira declaração, executa ela alterando o estado do programa, e depois executa a próxima até não existirem mais declarações ou o programa for finalizado por alguma instrução.

Podemos dizer que toda estrutura Java é uma declaração e toda declaração tem significado. Declarações em Java devem ser separadas por ; ou devem conter um Bloco de código. Tudo que devemos entender de uma declaração é que ela tem um significado e que elas são executadas em sequencia.

Vamos imaginar um outro código hipotético que é composto pela chama de 3 métodos. Tudo que podemos supor é que as três declarações são executadas em sequência, desde que não aconteça nada excepcional.

metodo1();
declaracao1();
metodo2();

3. Definição de Variáveis (Local Variable Declaration)

Variáveis são posições da memória usadas para armazenar informações necessárias durante a execução do código. Uma variável pode ser de um tipo primitivo (byte, short, int, long, float double, boolean ou char) ou um objeto. Tipos primitivos diferem de objeto porque armazenam apenas um valor sem ter nenhum método associado a ele. Uma variável irá pertencer a um bloco de código e sua existência existe do momento da declaração até a finalização do bloco. Como falamos, variáveis são posições de memória, mas existem dois tipos de memória em Java, que vamos falar posteriormente, a heap e a stack. Tipos primitivos são declarados na heap, enquanto objetos são declarados na heap, isso significa que para tipos primitivos cada variável é uma posição única enquanto um mesmo objeto pode ser compartilhado por várias variáveis.

— Ein?!?!

Sim! A princípio é difícil de entender. Todo bloco de código possui um valor associado a uma variável, no tipo primitivo temos na variável o valor exato enquanto nos objetos temos uma referência ao valor. Vamos demonstrar por um exemplo? Dê uma olhada no código abaixo. Observer que uma definição de variável sempre é acompanhada no formato <tipo> <nome da variável> = <valor>;.

int x = 0;    // x = 0
int y = x;    // x = 0,  y = 0
x = y + 20;   // x = 20, y = 0
y = 10;       // x = 20, y = 10

Usuario usr1 = new Usuário(1, "João"); // usr1 = Usuario[id=1, nome=João]
Usuario usr2 = usr1;                   // usr1 = Usuario[id=1, nome=João],     usr2 = Usuario[id=1, nome=João]
Usuario usr3 = new Usuário(1, "João"); // usr1 = Usuario[id=1, nome=João],     usr2 = Usuario[id=1, nome=João],     usr3 = Usuario[id=1, nome=João]
usr1.setNome("João Doe");              // usr1 = Usuario[id=1, nome=João Doe], usr2 = Usuario[id=1, nome=João Doe], usr3 = Usuario[id=1, nome=João]
usr1 = null;                           // usr1 = null,                         usr2 = Usuario[id=1, nome=João Doe], usr3 = Usuario[id=1, nome=João]

No código acima vemos os dois tipos de dados, temos a classe Usuario e temos o tipo primitivo int. Quando criamos uma variável do tipo primitivo a partir de outro valor, podemos alterar livremente o outro valor que a nova variável permanecerá inalterada. Mas o mesmo não acontece com a classe Usuario, que apesar de todos terem valores iguais, usr1 e usr2 por algum momento apontam para o mesmo objeto. usr3 nunca se altera porque é um objeto distinto mesmo tendo o mesmo valor que usr1 e usr2.

Na última linha do trecho de código usamos o valor null. null não é um tipo, apenas significa nulo em tradução livre, mas em computação significa a ausência de valor. É quando dizemos que uma variável não contem valor, ela não aponta para lugar nenhum. Variáveis que são tipos primitivos não pode ser nulas, elas deve sempre ter um valor associado.

4. Comentário (Comments)

Comentários são trechos de código que serão ignorados durante o processo de compilação. Apesar que alguns autores falam que todo comentário é uma falha, afirmação que eu discordo veementemente, eles são necessários para documentar informações que não podem ser documentadas no código. Tenha sempre em mente que comentários são necessários, com o tempo e a experiência você vai aprender sobre o que escrever nos comentários. Por enquanto vamos nos contentar em como comentar.

Existem 3 tipos de comentários em Java: 1. Comentários de fim de linha 2. Comentários tradicionais 3. Javadoc

Para criar um comentário em linha, adicione os dois caracteres // e tudo que você escrever até o fim da linha será desconsiderado durante a compilação. O exemplo abaixo foi retirado do código do Apache Kafka, apesar de estar em inglês ele contém informações relevantes ao código.

// Try to calculate partition, but note that after this call it can be RecordMetadata.UNKNOWN_PARTITION,
// which means that the RecordAccumulator would pick a partition using built-in logic (which may
// take into account broker load, the amount of data produced to each partition, etc.).
int partition = partition(record, serializedKey, serializedValue, cluster);

Para criar um comentário tradicional, inicie com / e todo caractere até encontrar o final / será desconsiderado. Abaixo temos mais um comentário retirado do código do Apache Kafka, ele explica a decisão de não existir um break naquela posição como veremos mais a frente.

case REAUTH_RECEIVE_HANDSHAKE_OR_OTHER_RESPONSE:
    handshakeResponse = (SaslHandshakeResponse) receiveKafkaResponse();
    if (handshakeResponse == null)
        break;
    handleSaslHandshakeResponse(handshakeResponse);
    setSaslState(SaslState.REAUTH_INITIAL); // Will set immediately
    /*
     * Fall through and start SASL authentication using the configured client
     * mechanism. Note that we have to either fall through or add a loop to enter
     * the switch statement again. We will fall through to avoid adding the loop and
     * therefore minimize the changes to authentication-related code due to the
     * changes related to re-authentication.
     */
case REAUTH_INITIAL:
    sendInitialToken();
    setSaslState(SaslState.INTERMEDIATE);
    break

O Javadoc é um tipo especial de comentário tradicional que nos permite gerar uma documentação oficial a partir do código. Ele se diferencia do comentário tradicional por iniciar com /*, não apenas /, e pode estar acima de classes, métodos e campos. Javadoc segue uma linguagem de marcação deve ser usada sempre, pois além de poder ser usada como documentação oficial, ela também será exibida pelas IDEs em funcionalidades que irão lhe auxiliar durante o desenvolvimento. Javadoc também aceita tags HTML, as não tente usar CSS e Javascript.

No exemplo abaixo temos um trecho da documentação oficial do Apache Kafka. É interessante notar que o autor desse código se preocupou em descrever a funcionalidade do método, e os motivos pelo qual as exceções são lançadas, mas ignorou a descrição do parâmetro porque é intuitivo. Evite comentários desnecessários.


/**
 * Get the partition metadata for the given topic. This can be used for custom partitioning.
 * @throws AuthenticationException if authentication fails. See the exception for more details
 * @throws AuthorizationException if not authorized to the specified topic. See the exception for more details
 * @throws InterruptException if the thread is interrupted while blocked
 * @throws TimeoutException if metadata could not be refreshed within {@code max.block.ms}
 * @throws KafkaException for all Kafka-related exceptions, including the case where this method is called after producer close
 */
@Override
public List<PartitionInfo> partitionsFor(String topic) {}
cap 02javadoc
Figura 3. O comentário acima gerou essa documentação.

Comentar código não é uma atividade simples, ela será trabalhada com a maturidade. Com o tempo você vai aprende que informações devem ser consideradas auxiliar ao código. Você não precisa comentar o que está no código, mas a informação que falta ao código, não é o como, mas o porque do código. Eu gosto de comentar pressupostos e escolhas arquiteturais porque em alguns meses eu não vou lembrar ou outra pessoa que pegar meu código também não vai saber o motivo de alguns escolhas.

5. Se (if)

Agora vamos ver a primeira declaração de fluxo que também é a mais comum. Mais conhecida como if, ou condicional, é composto por if (expressão booleana) <bloco> else <bloco>, onde expressão booleana é qualquer função que retorne um boolean ou uma expressão lógica que veremos em Operadores Lógicos. A expressão pode ser resumida para if (expressão booleana) <bloco> ou pode ser encadeada em várias outras declarações condicionais if (expressão booleana) <bloco> else if (outra expressão booleana) <bloco> else <bloco>.

int x = leNumeroInteiro();

if (x % 2 == 0) { // o operador % retorna o resto da divisão
    System.out.println("O valor lido é par!");
} else {
    System.out.println("O valor lido é impar!");
}

if (x % 3 == 0) {
    System.out.println("O valor lido é múltiplo de 3!");
} else if (x % 3 == 1) {
    System.out.println("O valor lido tem a forma f(x) = 3x + 1");
} else {
    System.out.println("O valor lido tem a forma f(x) = 3x + 2");
}

No exemplo acima temos 3 expressões lógica. A primeira calcula se o valor é par então logicamente o bloco else será executado para todo valor impar. A segunda calcula se o valor é divisível por 3, isso significa que o bloco else será chamado para todo valor não divisível, mas com o if encadeado fazemos a visão daquele que são no formato 3x + 1 e 3x + 2. Vamos ver as expressões mais a frente.

6. Enquanto (while)

Enquanto define que um bloco de código será executado até que uma expressão lógica seja falsa. A execução do bloco de código é feita continuamente logo depois do teste da expressão lógica. Exemplo?

int x = leValor();
while(x > 0) {
    System.out.println("Valor é positivo!");
    x = leValor();
}

O bloco de código acima será executado continuamente até que venha um valor 0 ou negativo.

7. Faça enquanto (do-while)

O Faça enquanto funciona de forma bem similar, a diferença é que o teste é feito depois que o bloco de código é executado. Ele é muito similar a declaração anterior, a diferença é a ordem de execução entre o teste lógico e o bloco de código.

do {
    executa();
} while (emExecução)

8. Para (for)

O famoso for é um pouco mais complexo. Ele é composto por 3 blocos que podem ser chamados de inicialização, condição e passo. Ao iniciar será executado uma única vez o trecho de código inicialização e em cada iteração será executado o trecho de código condição, que deve retornar uma expressão booleana, depois será executado o bloco de código para depois ser executado o trecho passo. O exemplo mais comum é para se iterar em um array.

int[] array = new int[] {0 , 1, 2, 3, 4, 5};
for (int i = 0; i < array.length; i++) {
    // bloco de código
}

9. Escolha (switch)

O switch escolhe o código de acordo com o valor de uma variável. O switch é uma estrutura que pode facilmente induzir a erros porque cada bloco não é exclusivo, o fluxo de execução passar de um bloco ao outro até que seja encontrada a instrução break. Vamos ver um exemplo?

int x = leValor();
switch (x) {
    case 1:
        System.out.println("É igual a 1!");
    case 2:
        System.out.println("É maior ou igual a 2!");
    case 3:
        System.out.println("É maior ou igual a 3!");
    case 4:
        System.out.println("É maior ou igual a 4!");
    case 5:
        System.out.println("É maior ou igual a 5!");
    default
        System.out.println("É maior que 5 ou menor que 1!");
}

O que aconteceria se o valor de x for igual a 3? Seriam executados os blocos de 3 até o default.

É maior ou igual a 3!
É maior ou igual a 4!
É maior ou igual a 5!
É maior que 5 ou menor que 1!

Se quisermos um valor exato, podemos usar o break:

int x = leValor();
switch (x) {
    case 1:
        System.out.println("É igual a 1!");
        break;
    case 2:
        System.out.println("É igual a 2!");
        break;
    case 3:
        System.out.println("É igual a 3!");
        break;
    case 4:
        System.out.println("É igual a 4!");
        break;
    case 5:
        System.out.println("É igual a 5!");
        break;
    default
        System.out.println("É maior que 5 ou menor que 1!");
}

Agora você deve ter se perguntado porque no texto do bloco default eu usei menor que 1? Isso porque o switch não é usado para intervalos de valores, mas para valores exatos e caso nenhum valor seja igual aos valores declarados é chamado o bloco default.

Vale lembrar que o switch pode ser usado para números, enumeradores e qualquer valor constante, inclusive String.

10. Quebra e continua (break e continue)

Uma quebra deve ser chamada dentro bloco switch, while, do ou for. Ao se deparar com essa instrução o programa irá finalizar a execução do bloco externo imediatamente.

Vamos demonstrar isso com um exemplo básico? No código abaixo vamos criar um for que será finalizado usando break. Observe que o ponto de parada do for seria no máximo inteiro possível, mas através do break finalizamos em 10.

System.out.println("Iniciando for...");
for (int i = 0; i < Integer.MAX_VALUE; i++) {
    System.out.println("Valor: " + i);
    if (i == 10) {
        break;
    }
}

Quando usamos break dentro de um switch evitamos que os blocos de códigos abaixo dele seja executados.

O continue tem um comportamento parecido, mas ao invés de finalizar o bloco será apenas finalizada a iteração. Ele só é aceito em iterações como while, do ou for. Vamos incrementar o exemplo acima para imprimir apenas números impares. Observe que no código abaixo foi preciso mudar a condição de execução do break porque ele nunca seria executado se usássemos i == 10.

System.out.println("Iniciando for...");
for (int i = 0; i < Integer.MAX_VALUE; i++) {
    if (i % 2 == 0) {
        continue;
    }
    System.out.println("Valor: " + i);
    if (i > 10) {
        break;
    }
}

Se você leu a documentação atentamente, viu que break e continue podem aceitar rótulos. O que isso significa? Vamos imaginar que temos um loop encadeado em que buscamos um valor dentro de uma matrix. Como as linhas dessa matrix são ordenadas, se o valor em uma coluna for maior que o valor desejado, podemos pular para próxima linha. A decisão do break e do continue é feita usando os rótulos que todo bloco de código aceita.

int[][] matrix = new int[][] {
        { 2, 2, 2, 3, 4, 5 },
        { 2, 4, 8, 8, 9, 9 },
        { 1, 2, 4, 5, 6, 8 },
        { 0, 3, 4, 8, 8, 9 },
        { 3, 4, 4, 6, 6, 9 },
        { 0, 3, 6, 7, 8, 8 },
};
linhas: for (int linha = 0; linha < matrix.length; ++linha) {
    colunas: for (int coluna = 0; coluna < matrix[linha].length; ++coluna) {
        if (matrix[linha][coluna] == 7) {
            System.out.println("Número 7 encontrado! (" + linha + "," + coluna + ")");
            break linhas;
        } else if (matrix[linha][coluna] > 7) {
            System.out.println("Desistindo da linha! (" + linha + "," + coluna + ")");
            continue linhas;
        } else if (matrix[linha][coluna] < 7) {
            System.out.println("Pulando para próxima coluna! (" + linha + "," + coluna + ")");
            continue colunas;
        }
        System.out.println("Código nunca executado!");
    }
}

Se não fosse usado um rótulo, o break e o continue iriam atuar somente no bloco de código mais interno.

11. Lance (throw)

O throw deve ser usado quando algo excepcional acontece. Algo inesperado, tanto que ele lança uma Exception, que significa exceção.

Exceções podem ser tratadas em código, mas as vezes elas não podem ser tratadas o que implica a finalização da execução. Ao se lançar uma exception, a JVM vai criar uma estrutura que contem o contexto da execução que chamamos de Stacktrace.

Para entender o que é uma Stacktrace, é preciso entender como um programa lida com contextos. Quando executamos um bloco de código é criado uma posição no topo da pilha de execução (stack é pilha em inglês). Ao terminar esse bloco, essa posição é removida da pilha. Vamos olhar o programa abaixo:

public class StacktraceHelloWorld {
    private static void m1(int x) {
        if (x % 2 == 0 && x > 100) {
            throw new RuntimeException("Primeiro número impar depois de 100");
        }
        m2(x + new Random().nextInt(2));
    }

    private static void m2(int j) {
        if (j % 2 == 0 && j > 100) {
            throw new RuntimeException("Primeiro número par depois de 100");
        }
        m1(j + new Random().nextInt(2));
    }

    public static void main(String[] args) {
        m1(0);
    }
}

A pilha vai ter como fundação o método main, depois ela será formada por um encadeamento de chamadas ao métodos m1 e m2. Nenhum dos elementos é removido da pilha porque os métodos nunca terminam, els ficam se chamando até que a exceção do tipo RuntimeException seja lançada.

Esse exemplo é meramente didático para mostrar como funciona o uso do throw. Mas se alterarmos o tipo de RuntimeException para apenas Exception vemos que não será possível de compilar porque há uma exceção não tratada (Unhandled exception type Exception). Isso acontece porque existem 3 tipos de exceções:

  1. Error

  2. RuntimeException

  3. Exception

Error não deve ser definido em um programa. Ele será lançado quando a JVM não souber lidar com uma situação especifica, o exemplo mais comum é o OutOfMemoryError quando a JVM não conseguir alocar mais memória.

Uma RuntimeException é uma exceção que acontece em tempo de execução, mas poderia ser resolvido com pequenas validações, ou seja, é algo deveria ter sido previsto. É o que acontece quando valores nulos não são validados (NullPointerException) ou quando acontece a divisão por zero (ArithmeticException).

Os demais casos devem estender a classe Exception, mas ela adicionará uma peculiaridade ao código. Se um método não trata um Exception, ele deve declarar que lança a mesma. Isso porque ela é um resultado esperado, mas que pode ou não ser tratado em código. Um exemplo? Quando estamos lidando com conexões de rede, sempre existe a possibilidade de a conexão ser finalizada, por isso sempre temos a IOException. Essa declaração se dá usando o throws e este não pode ser ignorado. Ou a exceção é tratado no método acima ou lançada para o próximo método.

public void conecta() throws IOException {
    // abre e fecha conexão
}

12. Sincronizado (synchronized)

synchronized deve ser usada com muita parcimônia. Nós vamos ver o seu uso mais a fundo quando formos falar de threads. Mas sendo sucinto, ela pode ser usada tanto para métodos quanto para objetos.

Para entender o conceito de sincronia, é preciso entender o que é paralelismo e concorrência. Eu tenho duas atividades que rodam em paralelo quando elas acontecem ao mesmo tempo e não há interferência entre si. Mas elas se tornam concorrentes quando existem recursos compartilhados que não podem ser acessados ao mesmo tempo.

Difícil de entender, não? Então vamos criar um modelo real. Digamos que uma loja tenha um livro caixa que deve registrar todas as vendas. Mas esse livro caixa só é atualizado no final do dia através das anotações de cada vendedor. Assim quando o vendedor realiza uma venda, ele faz uma anotação que depois será repassada para o livro caixa. As vendas acontecem em paralelo. Mas ao finalizar a venda existe o registro do estoque que é um caderno único que registra a entrada e saída de itens do estoque. Ou seja, quando o vendedor finaliza a venda, ele deve pegar o registro do estoque e adicionar uma saída. Se o vendedor A está em posse do registro, o vendedor B precisará ficar esperando, logo a baixa no caixa são operações concorrentes.

synchronized irá definir sob qual objeto será definida a sincronia da execução. Ele pode ser usado tanto para método (estático ou de instância) ou objeto avulso.

class Concorrente {
    public static synchronized void syncStaticMethod() {
        // Toda execução desse método será concorrente
    }

    public synchronized void syncMethod() {
        // Toda execução desse método será concorrente somente se for a mesma instância de Concorrente
    }

    public void method(Object lock) {
        synchronized (lock) {
            // Toda execução desse bloco será concorrente somente se a instância de lock for a mesma
        }
    }
}

Para que a sincronia seja bem elaborada, devem ser usados também os métodos wait, notify e notifyAll. Mas nós veremos como isso deve ser feito mais a frente, caso você precise lidar com valores compartilhados, prefira usar AtomicReference ou outras classes do pacote java.util.concurrent.atomic.

13. Operadores Lógicos

Os operadores lógicos do Java são usados para se criar expressões booleanas. Uma expressão booleana só pode retornar dois tipos de valores: verdadeiro ou falso.

Como vimos no uso do if, devemos sempre definir um valor booleano, mas as vezes ele pode ser uma série de valores encadeados em uma expressão.

É muito importante saber resolver esses tipos de expressão, essa é um campo da matemática que se chama Algebra Booleana e, na minha opinião, é um dos requisitos mais básicos para desenvolvimento de software.

No Java tempos três operadores booleanos &&, || e !

Operador Descrição Exemplo Significado

&&

E

a && b

true somente se a e b forem verdadeiras

||

OU

a || b

true qualquer um dos valores for verdadeiro

!

Negação

!a

true se a for `false e vice versa

14. Operadores Binários

Operadores binários realizam operações binárias. Para entender como funcionam operações binárias é preciso entender que toda informação é armazenada em formato binário, isso significa que o número 6544 é o mesmo valor de 0b0001100110010000 e 0x1990.

Operador Descrição

<<

Translada os bits para esquerda

>>

Translada os bits para a direita

&

Faz a operação E bit a bit

|

Faz a operação OU bit a bit

^

Faz a operação XOU bit a bit

~

Inverte (complemento) os valores dos bits

15. Operadores Matemáticos

Operadores matemáticos realizam operações matemáticas básicas.

Operador Descrição

+

Operador aditivo (também usado para concatenação de String)

-

Operador de subtração

*

Operador de multiplicação

/

Operador de divisão

%

Operador restante

16. Operadores Unários

Operadores unários realizam operações matemáticas básicas usando uma única variável. Os operadores unários mais comuns são ++ e -- que fazem duas operações sequenciais: retornam o valor e alteram o valor da variável. A posição do operador irá influenciar na ordem das operações. Veja o código abaixo a diferença.

int x = 0;    // x=0
int y = ++x;  // x=1 y=1
int z = 0;    // z=0
int w = z++;  // z=1 w=0

O operador unário pode ser usado também com expressões, mas para isso deve acompanhar o =. Veja no código abaixo.

int x = 0;         // x=0
x += 10;           // x=10
int y=2;           // x=10 y=2
x-=y;              // x=8 y=2
boolean w = true;  // w=true
boolean v != x;    // w=true v=false

17. Cast

O cast é uma conversão. Java é uma linguagem orientada a objetos, por isso todo valor estende a classe Object, mas todo valor tem uma própria classe. Usamos o cast em duas situações distintas, quando vamos lidar com classes mais especificas ou quando precisamos mudar o tipo de números.

O primeiro caso vamos ver mais a frente, já o segundo é quando precisamos alterar um tipo de valor para calculo matemático.

float x = 1.23121f;
int y = ((int) (x * 100.0f)) / 2;
System.out.println("x= " + x + " y=" + y);  // x= 1.23121 y=61

18. Operador condicional

O operador condicional é como se fosse um if em uma só linha. Ele é composto de uma expressão booleana e dois blocos que devem retornar um valor.

Vamos supor que precisamos calcular o valor absoluto de um número inteiro, isso pode ser feito com uma linha só.

void int abs(int valor) {
    return valor > 0 ? valor : -valor;
}

Exercícios

Os exercícios são propostos como forma de validar que você pode ir para o próximo passo. Para fixar o conteúdo dessa sessão implemente alguns algoritmos básicos como:

  1. Implemente a área do círculo

  2. Implemente o calculo da média aritmética

  3. Implemente o calculo da mediana

Para implementar os exercícios procure por // [EXERCÍCIO][CAP 02], implemente e execute mvn clean test para validar.

Originally published April 29, 2022