Técnicas de Entrada e Saída em Java e C

Prof. Jacques Sauvé
Agosto 1999, Setembro 2008

Dois motivos me levam a escrever o material que segue sobre manipulação de entrada e saída. A Quarta Olimpíada de Programação do DSC que acabou de ser realizada mostrou claramente as deficiências dos alunos na matéria. Além do mais, observei que muitos alunos usam a classe Console e cheguei à conclusão (tardia, talvez) que esta classe está muito mal feita e deve ser evitada. Minhas explicações para tanto aparecem no final deste material.

A Saída Padrão

Vamos começar falando de saída, já que é mais simples do que o tratamento de dados de entrada. A primeira pergunta que faço a respeito da saída é onde queremos ver a saída de um programa? A maioria dos alunos iniciantes responderão: "Na tela, é claro!". Well ... mais ou menos. Vamos topar essa resposta por enquanto. Em Java posso imprimir algo na tela usando

	System.out.println(...);

Em C, usaríamos:

	printf(...);

Em C++, seria:

	cout << ...;

Até aí, nada de novo: todo aluno faz assim. Agora, eu pergunto: "Mas se eu não quiser a saída na tela?" A saída na tela nem sempre é interessante por vários motivos:

Portanto, às vezes, quero guardar a saída de um programa em algum lugar em vez de imprimí-la na tela. Este "algum lugar" normalmente é um arquivo (poderia ser uma impressora, uma conexão de rede, etc. mas vamos só falar de arquivos aqui). Então a pergunta é: como fazer para que a saída do programa seja guardada num arquivo? Tem duas formas básicas de fazer isso: redirecionando a saída padrão e usando arquivos diretamente. Veremos a primeira alternativa agora e a segunda mais à frente.

Com respeito ao redirecionamento da "saída padrão", temos primeiro que dizer que System.out.println (e o equivalente em C e C++) não imprime sempre na tela. Imprime, na realidade, num lugar chamado a saída padrão. Se você executar o programa de forma normal, a saída padrão será, de fato, a tela. Porém, no momento em que executo o programa, posso redirecionar a saída padrão e fazer com que a saída vá para um arquivo. Em Windows, o redirecionamento da saída é feito como segue:

	C:\>programa >xxx

Desta forma, quando o programa terminar, a saída estará guardada no arquivo chamado "xxx" (ou qualquer outro nome que você quiser). Num sistema UNIX (Linux, por exemplo), o redirecionamento é feito da mesma forma.

Observe que o programa não sabe que este redirecionamento ocorreu. Seu programa continua imprimindo na saída padrão (System.out em Java, stdout em C, cout em C++) usando println, printf, etc. e o programa não sabe que a saída de fato foi para o arquivo "xxx". De fato, a próxima vez que você rodar o programa, poderá fazer assim:

	C:\>programa >yyy

ou assim:

	C:\>programa

No primeiro caso, a saída vai para o arquivo "yyy" e no segundo caso, a saída vai para a tela. Quem faz a mágica (o redirecionamento) sem o conhecimento do seu programa é o interpretador de comandos (cmd.exe no Windows, o shell no UNIX).

Isso é muito legal porque não amarra a saída de seu programa a um lugar específico. Você tem toda a liberdade de escolher onde vai a saída no momento em que executa o programa.

A Entrada Padrão

O que acontece na entrada é bastante semelhante ao que ocorre na saída. No seu programa você pode ler da entrada padrão e redirecioná-la quando executa o programa se quiser. Em Java, a entrada padrão chama-se System.in. Você talvez não a tenha usado se estiver usando a classe Console do livro Core Java ou a classe Scanner de Java, mas aguente um pouco que vou explicar tudo. Se estiver programando em C++, você deve ter usado cin; em C, a entrada padrão é lida com getchar() ou scanf().

Se eu não redirecionar a entrada padrão, a entrada vem do teclado. Para redirecionar a entrada padrão e ler um arquivo de entrada, usa-se a seguinte sintaxe ao chamar o comando:

	C:\>programa <ent

Aqui, o programa vai ler a entrada do arquivo "ent". Mais uma vez, o programa não sabe se está lendo do teclado ou de um arquivo armazenado em disco e isso é muito útil pois não amarra seu programa a um lugar específico para ler a entrada. Imagine se todo programa lesse apenas do teclado!! Se eu tivesse que fazer uma folha de pagamento, por exemplo, eu teria que digitar o nome, CPF, salário, etc. de todos os empregados da empresa todo mês!! Ufa!

Para quem está usando a classe Console em Java para fazer entrada de dados e ainda não está entendo muito bem o que danado é System.in, aguente um pouco. Estamos chegando lá.

Entrada de Dados

A questão de entrada de dados é bastante mais complexa do que a saída de dados porque a entrada depende de um ser humano e seres humanos são bichos complicados! Preciso aprofundar o assunto de entrada de dados com respeito a dois pontos cruciais:

Quando sei que a entrada acabou?

Vamos logo ao primeiro assunto: tem três formas de descobrir que a entrada acabou e que mais nada precisa ser lido:

Para exemplificar as três formas, vamos escrever um programa que some os inteiros presentes na entrada.

Usando um contador de itens

A entrada poderia ser assim:

5
1000
2000
500
1240
32

Se você estivesse digitando a entrada no teclado, você daria <ENTER> no final de cada linha. Observe que a primeira linha é um contador dizendo que seguem 5 linhas de dados. Em Java o programa seria assim (sem verificação de erros na entrada). Vou usar a classe Console por enquanto, mesmo que ela seja ruim:

import corejava.*;
public class Eco1 {
    public static void main(String[] args) {
        int numLinhas = Console.readInt("");
        int soma = 0;
        while(numLinhas-- > 0) {
            soma += Console.readInt("");
        }
        System.out.println(soma);
    }
}

Não usei System.in diretamente aqui porque quero falar disso à frente. Mas lembre que Console.readInt() usa System.in. Observe que dei um prompt vazio ("") ao readInt() porque não queria imprimir nada na tela além da entrada.

Em C, o programa seria assim:

void main(int argc, char *argv[]) {
    int numLinhas;
    int num;
    int soma = 0;
    scanf("%d", &numLinhas);
    while(numLinhas-- > 0) {
        scanf("%d", &num);
        soma += num;
    }
    printf("%d\n", soma);
}

Em C, scanf() lê da entrada padrão chamada stdin.

Usando um marcador de final

Podemos evitar o contador e simplesmente colocar um marcador no final da entrada. O problema é que nem sempre é óbvio que marcador pode ser usado. No exemplo, os dados de entrada são inteiros e, portanto, não podemos usar um inteiro como marcador porque poderíamos ter este mesmo valor nos dados de verdade e o programa se embanaria completamente. Por exemplo, se o marcador fosse "0", não poderíamos usar 0 nos dados em si. O mesmo ocorre com qualquer outro valor inteiro de marcador (neste caso de leitura de inteiros). Uma forma de tratar o problema aqui seria de usar um marcador final não numérico. Por exemplo, a entrada poderia ser:

1000
2000
500
1240
32
fim

Agora tudo bem, podemos diferenciar os dados do marcador. Mas não poderemos mais usar readInt() (em Java) para ler a entrada, pois "fim" não é numérico e o método readInt() vai reclamar. Temos que usar readString() e fazer a conversão de String para inteiro usando parseInt():

import corejava.*;
public class Eco2 {
    public static void main(String[] args) {
        String line;
        int soma = 0;
        while( !(line = Console.readString()).equals("fim") ) {
            soma += Integer.parseInt(line);
        }
        System.out.println(soma);
    }
}

Observe aqui que a linha com while faz muitas coisas:

Tudo isso numa única linha de código. Pode parecer um pouco obscuro mas é usado tão frequentemente que você tem que se acostumar. É o que chamamos uma "forma idiomática" de programar em Java.

Em C, teríamos o seguinte:

#define MAXLINHA 100
void main(int argc, char *argv[]) {
    char linha[MAXLINHA];
    int soma = 0;
    while(scanf("%s", linha) == 1 && strcmp(linha, "fim") != 0) {
        soma += atoi(linha);
    }
    printf("%d\n", soma);
}

O programa acima usa atoi() para converter de string para inteiro. Um dos problemas sérios do programa (que não vamos tratar aqui) é que uma linha de entrada muito grande pode fazer o array "linha" estourar.

Usando o fim da entrada

Se, em vez de colocar um contador no início ou um marcador no fim, colocássemos nada, nem no início, nem no fim? Será que o programa não poderia se virar e saber que a entrada acabou? A entrada acaba quando não tem mais dados para ler, ora mais!! Claro que podemos fazer isso. Lê-se a entrada até atingir o "fim do arquivo". A expressão "fim do arquivo" é usada mesmo que a entrada seja o teclado: significa "fim da entrada".

A entrada, portanto seria como segue. Nada de contador, nem marcador.

1000
2000
500
1240
32

Apresentamos o programa em Java abaixo. Não usamos a classe Console porque é justamente aí que ela começa a mostrar suas fraquezas. Na realidade, o método readString() da classe console retorna exatamente a mesma coisa (um string vazio) no caso de uma linha de entrada vazia ou no caso de atingir o fim do arquivo. Retornar a mesma coisa em duas situações diferentes é muito mau! É como estar dirigindo na estrada e o caminhão à sua frente piscar à direita. Você não sabe se ele vai dobrar à direita ou se está mandando passar! (Nunca obedeça nem faça esses sinais na estrada, viu?).

Então vamos deixar Console de lado e usar System.in. O System.in em si permite ler caracteres da entrada mas não linhas inteiras. Para ler uma linha inteira, transformamos System.in num BufferedReader. O método readLine() do BufferedReader retorna null quando não há mais nada na entrada.

import java.io.*;
public class Eco3 {
    public static void main(String[] args) {
	BufferedReader	inReader;
        inReader = new BufferedReader(new InputStreamReader(System.in));
        String line;
        int soma = 0;
        try {
            while((line = inReader.readLine())!= null) {
                soma += Integer.parseInt(line);
            }
            System.out.println(soma);
            inReader.close();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }
}

Depois que criamos o BufferedReader chamado inReader, as coisas são semelhantes ao uso de Console, mas com o onus adicional de tratar exceções. Parece pior ter que tratar exceções, mas não é assim. Todo bom programador tem que tratar condições de erro e exceções são o mecanismo para fazer isso. Esconder as exceções dentro da classe Console encoraja o programador a "esquecer" condições de erro, o que é um péssimo treinamento. Você pode pensar: "Mas, professor! Em vez de usar Console.readString(), usamos inReader.readLine(). Não dá na mesma, já que ambos os métodos retornam uma linha lida da entrada?". Não! Não dá na mesma! O motivo é que readLine() é um método que foi bem projetado: ele retorna um string vazio se a entrada contiver uma linha vazia e retorna outra coisa (null) quando a entrada acabou. Como, no programa acima, queremos saber se a entrada acabou, testamos o resultado de readLine()contra o valor null.

Em C, teríamos o seguinte:

#define MAXLINHA 100
void main(int argc, char *argv[]) {
    char linha[MAXLINHA];
    int soma = 0;
    while(scanf("%s", linha) == 1) {
        soma += atoi(linha);
    }
    printf("%d\n", soma);
}

Observe que a rotina scanf() retorna o número de item que ela conseguiu ler. Aqui, nós pedimos para ler um (1) item com "%s". Portanto, enquanto scanf() retornar 1, ele conseguiu ler o que pedimos e a entrada não acabou.

O programa em Java parece maior do que o programa em C, mas a versão Java é muito melhor pois é mais robusta (lembre que, em C, o array linha pode estourar).

Falta falar de mais uma coisinha para encerrar o assunto de "leitura até o fim da entrada". Se redirecionarmos a entrada padrão para que o programa leia de um arquivo, está claro o que "acabar a entrada" significa: simplesmente não há mais dados no arquivo. Mas o que significa isso se não redirecionarmos a entrada padrão e o programa estiver lendo do teclado? Sempre haverá entrada no teclado se eu continuar digitando coisas, certo? De que forma, então, posso avisar ao programa que a entrada acabou? Ocorre que cada sistema operacional traduz algumas teclas especiais para ações especiais. Você pode estar acostumado com a tecla "CONTROL-C" ou "CTRL-C". Quando você digita isso, o sistema operacional traduz o valor lido do teclado para uma ação: dar uma porrada no programa para que ele pare. Outras teclas têm um significado especial. Por exemplo, CTRL-S pára a saída (tem o mesmo efeito que a tecla Scroll Lock). A tecla CTRL-Z é usada justamente para representar o fim da entrada. No exemplo acima, você digitaria o seguinte no teclado:

1000<ENTER>
2000<ENTER>
500<ENTER>
1240<ENTER>
32<ENTER>
^Z<ENTER>

onde ^Z representa CONTROL-Z.

Acabamos de falar de três formas de saber quando parar de ler a entrada: usando um contador, usando um marcador e detectando o fim da entrada. Na Quarta Olimpíada, os três tipos de entrada foram usados! Pergunto agora: "Será que tem uma técnica melhor do que as outras ou é só questão de gosto?" A resposta é que:

Quantas coisas eu devo ler da entrada de cada vez?

Nosso segundo assunto sobre entrada de dados diz respeito ao tamanho do bocado que vamos ler da entrada. Há três formas típicas de ler a entrada:

Tem outras formas, mas essas são as principais. Como posso decidir entre essas formas de proceder? A chave é que você deve usar a técnica que mais se adeque à estrutura da entrada a ser lida e tratada. Vamos ver alguns exemplos:

  1. Eis um enunciado de um dos problemas da Olimpíada: "Dado um texto, imprima a quantidade de cada um dos caracteres alfabéticos nele presentes, em ordem alfabética de letra. Não há diferença de caixa (letras maiúsculas e minúsculas são consideradas idênticas). Não há letras acentuadas, nem ç, no texto (isto é, os caracteres são tirados do conjunto ASCII)."
    Neste caso, a melhor forma de resolver o problema é lendo um caractere de cada vez. Por quê? A leitura caractere-a-caractere é a mais simples de todas e deve ser usada quando possível. O problema acima fala de "linha"? Exige que eu leia uma linha inteira antes de saber o que fazer? Ou uma palavra inteira? Não! Lendo um único caractere, já posso processá-lo (isto é, neste caso, ver se é uma letra e incrementar a frequência da letra que ele representa).
  2. Eis o enunciado de outro problema da Olimpíada: "Números inteiros são dados na entrada. Você deve determinar quais deles são primos e imprimí-los. O formato da entrada é um número inteiro por linha, até o fim da entrada."
    Muito bem. Posso ler caractere a caractere aqui? Não, porque um número pode consistir de mais de um caractere. O enunciado menciona que a entrada está estruturada por linha e, portanto, faz sentido que meu programa leia a entrada de forma casada com sua estrutura.
  3. Um dos problemas da Olimpíada exigia a leitura de nomes e tamanhos de matrizes da entrada. Cada linha tinha uma definição de matriz contendo um nome, o número de linhas e o número de colunas da matriz. Por exemplo, a entrada poderia conter 9 definições de matrizes assim:

    A 50 10
    B 10 20
    C 20 5
    D 30 35
    E 35 15
    F 15 5
    G 5 10
    H 10 20
    I 20 25

    Aqui, temos que ler linha a linha (concorda? a entrada está estruturada por linha) mas devemos também quebrar uma linha lida em 3 informações distintas: um string e dois inteiros.

Agora que podemos reconhecer a forma de ler a entrada, falta saber fazê-lo em Java e C, certo? Mostraremos isso agora.

Leitura caractere a caractere

Em Java, podemos usar System.in diretamente, pois existe um método desta classe para ler um caractere. A princípio, parece que este método (que se chama read()) deveria retornar um item do tipo char, certo? Mas ele retorna um int! O motivo é que, além de poder retornar qualquer char, ele deve retornar algo especial para indicar o fim do arquivo. Foi decidido por quem escreveu este método que o valor -1 seria retornado para indicar o fim de arquivo. Como -1 não é um valor legal para um char, o método retorna um int. Parece mais complicado lendo essa explicação agora do que realmente fazer o programa. Veja abaixo a solução do problema de contar quantos caracteres a entrada contém:

import java.io.*;
public class Conta {
    public static void main(String[] args) {
        int numCarac = 0;
        int c; /* não é char! */
        try {
            while((c = System.in.read()) >= 0) {
                numCarac++;
            }
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
        System.out.println(numCarac);
    }
}

Em C, temos o mesmo problema de char versus int indicado acima. getchar() é a rotina a usar para ler um caractere e o valor inteiro especial EOF (igual a -1) é usado para indicar o fim do arquivo. EOF está definido no arquivo stdio.h.

#include <stdio.h>
void main(int argc, char *argv[]) {
    int numCarac = 0;
    int c; /* nao eh char! */
    while((c = getchar()) != EOF) {
        numCarac++;
    }
    printf("%d\n", numCarac);
}

Leitura linha a linha

Já vimos exemplos da leitura linha a linha acima. Veja o programa Eco3.

Leitura linha a linha com separação de palavras

As vezes, a linha que temos na mão deve ser quebrada em "palavras" (também chamadas "tokens") e cada palavra tratada separadamente. Por exemplo, vamos pegar o exemplo de matrizes acima e imprimir na saída aquilo que encontramos na entrada. Por exemplo, a saída poderia ser

        Encontrei uma matriz chamada A com 50 linhas e 10 colunas
        Encontrei uma matriz chamada B com 10 linhas e 20 colunas
        ...

Em Java, podemos usar um StringTokenizer para quebrar um String em pedaços:

import java.io.*;
import java.util.*;
public class Mat {
    public static void main(String[] args) {
	BufferedReader	inReader;
        inReader = new BufferedReader(new InputStreamReader(System.in));
        String line;
        try {
            while((line = inReader.readLine())!= null) {
                StringTokenizer st = new StringTokenizer(line);
                String nome = st.nextToken();
                String numLinhas = st.nextToken();
                String numColunas = st.nextToken();
                System.out.println("Encontrei uma matriz chamada " + nome +
                    " com " + numLinhas + " linhas e " + numColunas + " colunas");
            }
            inReader.close();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }
}

No exemplo acima, new StringTokenizer(line) vai separar a linha em pedaços separados por "espaços em branco". Um "espaço em branco" pode ser um ou mais espaços, tabs ou caracteres de nova-linha.

Em C, este exemplo é mais simples porque a entrada formatada é simples nesta linguagem:

void main(int argc, char *argv[]) {
    char nome[100];
    int numLinhas;
    int numColunas;
    while(scanf("%s %d %d", nome, &numLinhas, &numColunas) == 3) {
        printf("Encontrei uma matriz chamada %d com %d linha e %d colunas\n",
            nome, numLinhas, numColunas);
    }
}

Como outro exemplo, vamos supor que eu queira imprimir na saída todas as palavras encontradas na entrada, imprimindo uma palavra por linha. Observe que não sabemos quantas palavras podem estar numa mesma linha. Uma possível solução segue abaixo (há soluções melhores mas não é o momento de falar delas).

import java.io.*;
import java.util.*;
public class Quebra {
    public static void main(String[] args) {
	BufferedReader	inReader;
        inReader = new BufferedReader(new InputStreamReader(System.in));
        String line;
        try {
            while((line = inReader.readLine())!= null) {
                StringTokenizer st = new StringTokenizer(line);
                while(st.hasMoreTokens()) {
	            System.out.println(st.nextToken());
                }
            }
            inReader.close();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }
}

Em C, teríamos:

void main(int argc, char *argv[]) {
    char nome[10];
    while(scanf("%s", nome) == 1) {
        printf("%s\n", nome);
    }
}

Apesar de C ser muito mais simples que Java neste caso, não condene a linguagem Java completamente. Ela é muito melhor que C, mas tem algumas fraquezas, principalmente a entrada e saída formatadas, o assunto sob discussão. Não é justo julgar Java apenas com tais exemplos. Posso mostrar outros exemplos (acesso a rede, por exemplo), onde um programa de 10 linhas em Java precisa de 100 linhas em C.

Saída Formatada

Acabou a discussão sobre entrada de dados acima. A saída de dados é muito mais simples. Já estamos usando System.out.println() e printf() sem problemas. Só falta um assunto sobre saída: como fazer saída formatada? Em C, a solução é simples, pois printf() tem excelentes opções de formatação. Veja a documentação da linguagem.

Em Java, você pode fazer algo equivalente a C para impressão formatada usando a classe java.util.Formatter.

Vamos fazer um exemplo em C. Queremos ler a entrada de matrizes acima e imprimir a saída com o nome num campo de 20 colunas justificado à esquerda, e os campos de linhas e colunas com 10 posições com justificação à direita.

void main(int argc, char *argv[]) {
    char nome[100];
    int numLinhas;
    int numColunas;
    while(scanf("%s %d %d", nome, &numLinhas, &numColunas) == 3) {
        printf("%-20.20s %10d %10d\n", nome, numLinhas, numColunas);
    }
}

Manipulação de Arquivos com Nomes

Na discussão acima, manipulamos arquivos armazenados em disco através do recurso de redirecionamento de entrada e saída. Em várias situações, entretanto, precisamos manipular arquivos através do seu nome. Isto seria necessário, por exemplo, se um programa tivesse que manipular mais do que um arquivo de cada vez. Neste caso, a entrada padrão poderia ser usada para acessar um desses arquivos, mas não ambos.

Praticamente nada muda nos exemplos que já mostramos quando acessamos arquivos pelo nome. Em Java, por exemplo, só temos que que criar um BufferedReader a partir do nome do arquivo. Veja o exemplo abaixo:

import java.io.*;
import java.util.*;
public class Mat3 {
    public static void main(String[] args) {
        String line;
        BufferedReader	inReader = null;
        try {
            inReader = new BufferedReader(
                            new FileReader("entrada.txt"));
        } catch( FileNotFoundException e ) {
            System.err.println("Não pode abrir entrada.txt");
            System.exit(1);
        }
        try {
            while((line = inReader.readLine())!= null) {
                StringTokenizer st = new StringTokenizer(line);
                String nome = st.nextToken();
                String numLinhas = st.nextToken();
                String numColunas = st.nextToken();
                System.out.println("Encontrei uma matriz chamada " + nome +
                    " com " + numLinhas + " linhas e " + numColunas + " colunas");
            }
            inReader.close();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }
}

Como você pode ver, mudou apenas a parte inicial que abre o arquivo de entrada (chamamos de "abrir o arquivo" a operação que disponibiliza um arquivo para leitura ou escrita a partir de seu nome). Aqui, o nome do arquivo é "entrada.txt" e o arquivo aberto é acessado com inReader. Em vez de criar um BufferedReader a partir da entrada padrão (System.in), nós a criamos a partir do nome do arquivo.

Observe que o programa acima trata exceções na abertura do arquivo. O mundo é cruel e o programador tem que estar atento a possíveis falhas. Por exemplo, o que ocorre se eu tentar abrir um arquivo inexistente? O código acima mostra como proceder.

Além do mais, o leitor atento terá observado três pontos adicionais sobre o exemplo acima:

  1. Quando ocorre um erro, usamos System.err em vez de System.out. Isso ajuda a manter os erros separados da saída padrão normal. System.err é chamada de "saída padrão de erro".
  2. Para terminar um programa no meio do código, você pode chamar System.exit(n). Se o valor de n for 0, você está indicando que o programa executou perfeitamente. Se o valor de n for diferente de 0 (como no exemplo acima), você está indicando que houve erro no programa.
  3. Um arquivo que você abre sempre deve ser "fechado" antes do fim do programa. Isso foi feito no progama aterior com inReader.close().

Mais uma vez, esta é a forma idiomática de programar em Java.

Poderíamos também deixar a saída num arquivo cujo nome sabemos:

import java.io.*;
import java.util.*;
public class Mat4 {
    public static void main(String[] args) {
	BufferedReader	inReader = null;
        PrintWriter     outWriter = null;
        String line;

        try {
            inReader = new BufferedReader(
                            new FileReader("entrada.txt"));
        } catch( FileNotFoundException e ) {
            System.err.println("Não pode abrir entrada.txt");
            System.exit(1);
        }
        try {
            outWriter = new PrintWriter(
                            new FileWriter("saida.txt"));
        } catch( FileNotFoundException e ) {
            System.err.println("Não pode abrir saida.txt");
            System.exit(1);
        } catch ( IOException e ) {
            System.err.println(e.getMessage());
            System.exit(1);
        }

        try {
            while((line = inReader.readLine())!= null) {
                StringTokenizer st = new StringTokenizer(line);
                String nome = st.nextToken();
                String numLinhas = st.nextToken();
                String numColunas = st.nextToken();
                outWriter.println("Encontrei uma matriz chamada " + nome +
                    " com " + numLinhas + " linhas e " + numColunas + " colunas");
            }
            inReader.close();
            outWriter.close();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }
}

Em C, o programa seria como segue:

#include <stdio.h>
void main(int argc, char *argv[]) {
    char nome[100];
    int numLinhas;
    int numColunas;
    FILE *fpin, *fpout;

    if((fpin = fopen("entrada.txt", "r")) == NULL) {
        fprintf(stderr, "Nao pode abrir entrada.txt\n");
        exit(1);
    }
    if((fpout = fopen("saida.txt", "w")) == NULL) {
        fprintf(stderr, "Nao pode abrir saida.txt\n");
        exit(1);
    }
    while(fscanf(fpin, "%s %d %d", nome, &numLinhas, &numColunas) == 3) {
        fprintf(fpout, "Encontrei uma matriz chamada %s com %d linha e %d colunas\n",
            nome, numLinhas, numColunas);
    }
    fclose(fpin);
    fclose(fpout);
}

Por que a Classe Console é Ruim?

De forma geral, a classe Console do livro Core Java foi muito mal projetada por quem a fez. Eis os motivos:

  1. O método readString() não diferencia uma linha vazia do fim da entrada. Em ambos os casos, retorna um string vazio (""). O mesmo se aplica ao método readWord().
  2. Os métodos escondem o tratamento de exceções. Embora isso possa parecer interessante para o aluno principiante, é péssimo para o programador profissional. Erros devem ser tratados no lugar correto e tratá-los dentro dos métodos de leitura não é uma boa. Apenas quem chama os métodos de leitura sabe o que fazer em caso de erro.
  3. Alguns métodos (readInt()por exemplo) só podem ser chamados fornecendo um prompt a ser colocado na saída para pedir os dados. Nem sempre quero jogar uma mensagem na tela para ler algo da entrada. Você pode achar que posso eliminar este problema dando um prompt vazio: readInt(""). Não funciona como você quer devido ao próximo motivo.
  4. Os métodos acrescentam um branco no final do prompt fornecido como parâmetro antes de colocá-lo na saída padrão. Isto significa que o que aparece na saída padrão nunca será exatamente aquilo que você pediu.
  5. readInt() usa readString() e, portanto, não trata o fim de arquivo corretamente. Não há forma de usar a classe Console se eu não tiver certeza se a entrada terminou ou não.

Resumindo: a classe Console só serve para brincadeiras mas não para desenvolvimento profissional. Ela é um excelente exemplo de como não fazer uma classe. Sua interface com o mundo externo foi muito mal bolada.