C, PHP, VB, .NET

Дневникът на Филип Петров


* Стандартен вход и изход

Публикувано на 13 септември 2009 в раздел ПИК3 Java.

Любителите на Linux много харесват Java заради концепцията на езика да работи с входни  и изходни потоци. Това означава една абстракция на входните и изходните данни, които използва и връща програмата. Така чрез дребна настройка може една програма вместо да изкарва данни на екрана – да ги записва във файл, да ги изпрати до принтер, порт на компютъра, друга програма, да ги изпрати към сървър в интернет и т.н.; и обратно – да ги получава от най-различни източници. Независимо с какви данни работят потоците, ние можем да ги разглеждаме просто като последователни парчета данни. Входни са потоците, които програмата получава, а изходни са потоците, които изпраща. Засега обаче ще разгледаме само няколко примера за методи, които работят със стандартния вход и изход (клавиатура и монитор) и само ще загатнем как се пренасочват. В следващата статия ще разгледаме как се чете и пише във файл и как можем да пренасочваме стандартните потоци към файлове.

1. Стандартен изход: най-просто казано стандартния изход е отпечатване на информацията на екрана на компютъра (това впрочем може да бъде променено). Ние вече показахме няколко примера с функцията System.out.format(). По-популярни обаче са System.out.print() и System.out.println(). При тях можем да конкатенираме различни типове данни като символен низ. За всеки от тях поотделно се извиква метод „toString()“ (преобразува данните в тип String), след което получените низове се конкатенират и изпращат към стандартния изход (монитора) във вид на низ. Ето един пример:

    int i=2;
    double d=3;
    System.out.print("i = "+i+", and d = "+d+"\n");

Виждате, че миксирахме различни типове данни без проблем. Методът println() действа по същия начин. Разликата с print е само една – при println() се слага автоматично символ за нов ред \n в края на низа. Следващия пример е напълно еквивалентен на първия:

    int i=2;
    double d=3;
    System.out.println("i = "+i+", and d = "+d);

Тук е мястото да отбележим, че знакът „+“ може да предизвика объркване. Следните два реда например връщат коренно различен резултат:

    System.out.println("Result: "+2+3);
    System.out.println("Result: "+(2+3));

Първото ще върне резултат „Result: 23“ (тоест ще отпечати първото число и после второто), а второто ще върне резултат „Result 5“, т.е. първо е изчислило изразът в скобите и след това го е отпечатило. Затова бъдете внимателни. Трябва да отбележим, че методът „format“ има изключително богати възможности, като отличителни са операциите с дата и час. Няма обаче да се спираме подробно на тях , но ви насърчаваме да ги прочетете от документацията на Java.

2. Стандартен изход за грешки: абсолютно същите методи както в System.out ги има и в System.err. Това се нарича „стандарен изход за грешки“. Идеята за такова дублиране е да има разделение между стандартната информация и тази, която се изписва при непредвидени ситуации. По подразбиране стандартния изход за грешки отпечатва на екрана. Понякога обаче се използва за правенето на логове (т.нар. dump файлове). Ще използваме System.err когато по-късно изучим „обработка на изключения“.

3. Стандартен вход: „обратните“ на System.out методи се дефинират в System.in. Стандартния вход за всеки компютър по подразбиране е клавиатурата. Още в началото искаме да споменем, че за разлика от System.out което има функционалност за работа със символи, при System.in винаги се работи с байтове. Методът, който се използва за четене на информация байт по байт е System.in.read(). Въпреки, че резултатът е byte, ние трябва да го присвояваме в променлива от тип int. Това е така, защото при грешка (или прекъсване) се връща -1 (допълнителна стойност, за която трябва да има отделено място). Следният пример чете информация от клавиатурата байт по байт до натискане на Enter:

    int input;
    System.out.print("Enter text: ");
    do{
      try{
        input = System.in.read();
        System.out.print((char)input);
      }
      catch(java.io.IOException e){
        System.err.println("ERROR READING");
        break;
      }
    }while(input!=(int)'\n');

Засега приемете изписаните try-catch блокове като даденост – по-късно ще ги разгледаме подробно. Тук се сблъскваме с първата „странност“ в поведението на System.in.read(). Резултатът от изпълнението на програмата ще бъде следното:

Enter text: <текст, който вие пишете>
<текстът, който сте написали>

тоест ако сте написали от клавиатурата „Misho“ то на екрана ще се появи следното:

Enter text: Misho
Misho

Защо се получи така? Точно след изпълнението на System.in.read() ние правихме System.out.print(…). Би било редно след прочитане на байт той да се изпише моментално?!? Полученият резултат е нормален поради спецификата на методът read(). На практика когато бъде извикан метода, програмата става в режим на изчакване да въведете информация. Да, но по дефиниция информация се въвежда след натискане на бутон Enter. Тоест още при първото извикване на read() ние можем да въведем повече от един символ – от примера с „Misho“ това са 5 символа. Чак тогава натискаме бутон Enter и изпълнението на програмата преминава към операторът System.out.print(…). За да го демонстрираме по-добре нека преправим предишния код така, че вместо при \n да се спира при символ s:

    int input;
    System.out.print("Enter text: ");
    do{
      try{
        input = System.in.read();
        System.out.print((char)input);
      }
      catch(java.io.IOException e){
        System.err.println("ERROR READING");
        break;
      }
    }while(input!=(int)'s');

Резултатът при написването на „Misho“ ще бъде следния:

Enter text: Misho
Mis

Виждате, че „h“ и „o“, както и символът за нов ред не се отпечатаха! Тогава къде се „изгубиха“? Отговорът е, че те останаха в буферът на клавиатурата. Получи се следното:

– При първото „завъртане“ на цикъла в буфера на клавиатурата записахме последователността от символи (във вид на байтове) „Misho\n“. На променливата input подадохме първата буква, която след това се отпечата на екрана;

– При второто „завъртане“ на цикъла ние имаме в буферът символите „isho\n“. Така вече не се извиква поле искащо входни данни, а веднага се взима буквата ‘i’, която после се отпечатва на екрана;

– При третото „завъртане“ на цикъла се взима буквата ‘s’ и се отпечатва. Тук заради условието на цикъла той прекъсва. Така на екрана остават отпечатани само буквите „Mis“. Тук веднага трябва да ви направи впечатление нещо много важно. В буферът на клавиатурата са останали символите „ho\n“! Това би могло да доведе до значителни проблеми по-нататък в програмата, защото ако отново ви се наложи да четете от клавиатурата, то вие ще вземете директно оставащите букви от буфера на клавиатурата! Ето как можем да покажем това:

    int input;
    System.out.print("Enter text: ");
    do{
      try{
        input = System.in.read();
        System.out.print((char)input);
      }
      catch(java.io.IOException e){
        System.err.println("ERROR READING");
        break;
      }
    }while(input!=(int)'s');

    try{
       System.out.println((char)System.in.read());
    }
    catch(java.io.IOException e){
       System.err.println("ERROR READING");
    }

При изписване на думата „Misho“ резултатът ще е следния:

Enter text: Misho
Mish

Виждаме, че System.out.println((char)System.in.read()); отпечата символът „h“, т.е. се случи точно това, което очаквахме – в буферът има информация! Освен това дори след това четене в буферът са останали символите „o\n“. За да се справим с подобни ситуации ние трябва да „почистваме“ буфера от излишната информация. Най-лесно това става по следния начин:

    try{
      while(System.in.available() > 0) System.in.skip(System.in.available());
    }
    catch(java.io.IOException e){
       System.err.println("ERROR READING");
    }

System.in.available() връща int с броя байтове, които са налични в буфера. System.in.skip() пък приема число от тип long, с което се пропускат определен брой байтове (т.нар. flush). Именно заради разликата в типовете се налага да използваме цикъл, тъй като буферът може да има повече от 32 бита (един int) и така методът available() да не връща точна информация. Всъщност съвсем възможно е да имаме огромни количества данни чакащи в този буфер.

4. Форматиран вход: след Java 5 се наложи един доста полезен стандартен обект – Scanner. Чрез „скенерът“ ние можем да се възползваме от форматиране на входните данни. При него четенето от входния поток става не байт по байт, а чрез типове данни. Следният пример чете всичко до достигане на интервал (space) и го отпечатва на екрана:

    java.util.Scanner s = null;
    String input;
    s = new java.util.Scanner(System.in);
    System.out.print("Enter text: ");
    input = s.next();
    System.out.print(input);

В обект Scanner обаче има много по-мощни методи. Погледнете следния пример:

     String input = "Hello 123 World";
     java.util.Scanner s = new java.util.Scanner(input);
     String s1 = s.next();
     int i = s.nextInt();
     String s2 = s.next();
     System.out.println(s1+s2+i);
     s.close();

Първо забележете последния ред – тук затваряме скенера. В предишния пример не го направихме, понеже той работеше със System.in – не бихме изкали да си затворим буфера на клавиатурата! Тук обаче това е добре да бъде направено. Виждате, че можем да „очакваме“ четене на целочислен тип. Аналогични са методите nextBoolean(), nextByte(), nextShort(), nextLong(), nextFloat() и nextDouble(). За изчистване на буфера има вече познатия skip(). За четене на цял ред (тоест „оставащото до края на текущия ред“) има много често използвана функция nextLine() връщаща String. Също така много удобни са функциите hasNextInt(), hasNextDouble() и т.н. – те връщат true или false за това дали има следващ елемент от такъв тип. Пример: Намира първите намерени числа от подаден String и ги отпечатва на екрана:

     String input = "9,-12,4,a,32";
     java.util.Scanner s = new java.util.Scanner(input);
     s.useDelimiter(",");
     while (s.hasNextInt()) System.out.print(s.nextInt()+" ");
     s.close();

Резултат: 9 -12 4 Видяхте и още една функция – useDelimiter(). Тя променя стандартният разделител от интервал в зададения String. С тези примери впрочем демонстрирахме и възможността да четем от различни източници (в случая от String вместо от клавиатурата). В следващата статия ще покажем как се чете и пише във файл и как се пренасочва стандартния изход. Внимание: При подаване на грешни данни в Scanner може да се очаква InputMismatchException, тоест „несъвпадащи данни“. В такъв случай програмата ще се прекрати с това съобщение за грешка.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*