среда, 1 августа 2012 г.

io: byte-streams

    Если byte[] представляет собой встроенный в язык тип, представляющий собой данные в памяти компьютера (ОЗУ==RAM), то InputStream / OutputStream представляют собой классы стандартной библиотеки (JDK) для ввода/вывода (чтения/записи) байтов.
    Основные отличия byte[] от InputStream/OutputStream:
    1) у byte[] есть размер (byte[].length), а у IS/OS - нет, что позволяет читать как неограниченные по размеру объемы данных (length - имеет тип int, и максимальное значение порядка 2.000.000.000) - 10, 100, 1000 Gb, так и данные неизвестного заранее размера - поток видеоданных с сервера при видео-конференции.
    2) byte[] - один тип И для чтения И для записи, IS/OS - два разных.
    3) byte[] - целиком лежит в памяти и, значит, убирается автоматически (GC), IS/OS - могут содержать компоненты вне памяти, которые надо явно закрывать методом close().
    4) byte[] - данные с произвольным доступом, IS/OS - с последовательным.

    Обратите внимание, InputStream / OutputStream содержат метод close(). Необходимо обеспечить из вызов по окончанию работы с потоком (успешным или безуспешным). С потоком ввода/вывода возможно связаны ресурсы за пределами Java heap. 
    Пример: ByteArrayInputStream / ByteArrayOutputStream - не связаны, все уберет GC.
    Пример: FileInputStream / FileOutputStream - скорее всего связаны "магические структуры" операционной системы, файловые дескрипторы.
    Пример: работа с интернетом - точно связаны классы JDK - java.net.Socket + "магические структуры" нашей операционной системы = сокеты (socket) + какие-то ресурсы сетевой карты (порты, прерывания, память) + сокет на второй стороне (на сервере).
    При вызове close() эти ресурсы освобождаются.


    InputStream

    Замечание: далее везде ниже в файле "c:/tmp/text.txt" лежит строчка "Hello World!" (без кавычек - "). Файл занимает 12 байт.


    Этот пример кода показывает, что разработчики JDK положили в основу получения байтовых данных из различных источников (файл, интернет, массив) один специальный тип - InputStream. Можно сказать, что этот тип представляет собой Pure Fabrication (Чистая Синтетика) - абстракция, не соответствующая чему-либо конкретному из предметной области
import java.io.*;
import java.net.URL;

/**
 * BAD! You MUST close InputStreams and OutputStreams always!
 */
public class _0_ISTest {
    public static void main(String[] args) throws IOException {

        InputStream inFile 
                = new FileInputStream("c:/tmp/text.txt");
        readFullyByByte(inFile);
        System.out.print("\n\n\n");

        InputStream inUrl 
                = new URL("http://google.com").openStream();
        readFullyByByte(inUrl);
        System.out.print("\n\n\n");

        InputStream inArray 
                = new ByteArrayInputStream(new byte[] {65, 66, 67, 68, 69});
        readFullyByByte(inArray);
        System.out.print("\n\n\n");
    }

    public static void readFullyByByte(InputStream in) throws IOException {
        while (true) {
            int oneByte = in.read();
            if (oneByte != -1) {
                System.out.print((char) oneByte);
            } else {
                System.out.print("\n" + "end");
                break;
            }
        }
    }
}


    Два улучшение предыдущего примера:
    1. Корректно закрываем поток ввода.
    2. "Укороченная форма" чтения из InputStream.
    Справка (упрощенное описание, может не совпадать с точным от авторов компиляторов):
    Statement - все, после чего можно поставить точку с запятой. Пример: Math.sin(1); int x; int y = 1;
    Expression - все, что имеет значение, т.е. может стоять в правой части оператора присваивания. Пример: Math.sin(1), 123, "Hello World!", 2 * (2 + 30 / 2).
    Некоторые части кода являются И Statement И Expression. Пример: Math.sin(1). Присвоение является И тем И другим. Это позволяет писать такое:
        int x, y, z;
        z = 1;        
        y = (z = 2);
        x = (y = (z = 3));    
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class _1_ISTest_close_short {
    public static void main(String[] args) throws IOException {
        String fileName = "c:/tmp/text.txt";

        InputStream inFile = null;
        try {
            inFile = new FileInputStream(fileName);
            readFullyByByte(inFile);
        } catch (IOException e) {
            throw new IOException("Exception when open+read file " + fileName, e);
        } finally {
            if (inFile != null) {
                try {
                    inFile.close();
                } catch (IOException ignore) {
                    /*NOP*/
                }
            }
        }
    }

    public static void readFullyByByte(InputStream in) throws IOException {
        int oneByte;
        while ((oneByte = in.read()) != -1) {
            System.out.print((char) oneByte);
        }
    }
}
    Вопросы по примеру:
1) зачем нужна проверка "if (inFile != null) {...}" ?
2) зачем приведение типа перед выводом на консоль "(char) oneByte"?
3) разберитесь, как именно преобразуется byte в int при чтении методом "int read()". Напишите такое преобразование самостоятельно (помните, byte лежит в диапазоне [-128, 127], а соответствующий int в диапазоне [0, 255]):
    byte b0 = 10;
    int x0 = ... что-то сделать с b0;
    byte b1 = -10;
    int x1 = ... что-то сделать с b1;


    int x2 = 10;
    byte b2 ... что-то сделать с x2;

    int x3 = 210;

    byte b3 ... что-то сделать с x3;




    Более оптимально (меньше обращений к программно-аппаратным (винту, сетевой карте, драйверам, модулям операционной системы) устройствам ввода/вывода) вычитывать данные не побайтно, а диапазонами байт (в буфер): 
import java.io.*;
import java.util.Arrays;

public class _2_ISTest_array {
    public static void main(String[] args) throws IOException {
        String fileName = "c:/tmp/text.txt";

        InputStream inFile = null;
        try {
            inFile = new FileInputStream(fileName);
            readFullyByArray(inFile);
        } catch (IOException e) {
            throw new IOException("Exception when open+read file " + fileName, e);
        } finally {
            closeQuietly(inFile);
        }
    }

    public static void readFullyByArray(InputStream in) throws IOException {
        byte[] buff = new byte[5];
        while (true) {
            int count = in.read(buff);
            if (count != -1) {
                System.out.println("count = " + count
                        + ", buff = " + Arrays.toString(buff)
                        + ", str = " + new String(buff, 0, count, "UTF8"));
            } else {
                break;
            }
        }
    }

    private static void closeQuietly(InputStream inFile) {
        if (inFile != null) {
            try {
                inFile.close();
            } catch (IOException ignore) {
                /*NOP*/
            }
        }
    }
}
    Примечание: обратите внимание на различие в смысле int, который возвращают методы "int read()" и "int read(byte[])". Если вернули -1, то смысл одинаков - больше в потоке нет данных. Но если вернули не -1, то "int read()" возвращает прочитанный байт, преобразованные к типу int, т.е. сами данные, а "int read(byte[])" возвращает количество данных в буфере.



    "Укороченная форма" чтения буфером:
import java.io.*;

public class _3_ISTest_array_short {
    public static void main(String[] args) throws IOException {
        String fileName = "c:/tmp/text.txt";

        InputStream inFile = null;
        try {
            inFile = new FileInputStream(fileName);
            readFullyByArray(inFile);
        } catch (IOException e) {
            throw new IOException("Exception when open+read file " + fileName, e);
        } finally {
            closeQuietly(inFile);
        }
    }

    public static void readFullyByArray(InputStream in) throws IOException {
        byte[] buff = new byte[5];
        int count;
        while ((count = in.read(buff)) != -1) {
            System.out.println(new String(buff, 0, count, "UTF8"));
        }
    }

    private static void closeQuietly(InputStream inFile) {
        if (inFile != null) {
            try {
                inFile.close();
            } catch (IOException ignore) {
                /*NOP*/
            }
        }
    }
}




    OutputStream

    Абстрактный класс OutputStream имеет 5 методов, объединенных в 2 группы: 
    Запись: write(int)write(byte[])write(byte[], int, int)
    Завершение: flush()close().

    Надо понимать, что есть контракт класса OutputStream, а есть контракт конкретного наследника. Они могут не совпадать, но контракты наследников не могут противоречить контракту предка! Т.е. код, написанный для работы с предком, должен корректно работать с любым потомком.

    В общем случае, предполагается, что у потока вывода есть два состояние - "готов к записи" и "закрыт" (по факту, может быть всего единственное состояние, как у ByteArrayOutputStream -"Closing a ByteArrayOutputStream has no effect", flush() - унаследован от OutputStream, тоже ничего не делает). У потока вывода нет состояния "новый", это выражается в том, что нет методов init()/open()/connect()/start()/... Сразу же после создания, поток готов к записи. После вызова close() - поток закрыт. Невозможно повторно открыть закрытый поток.

    Общая практика завершения работы с потоком вывода заключается в вызове flush() + close(). Для некоторых потоков эти методы могут ничего не делать, как у ByteArrayOutputStream. Но никто еще не был уволен за вызов этой комбинации. close() стоит вызывать в finally секции, в close() могут освобождаться важные системные ресурсы (FileOutputStream - может вызывать освобождение ресурсов операционной системы, связанных с файлом).

    "Пишущие" методы
    Запись одного байта производится методом write(int). По ряду причин (TODO: каких?)сигнатура метода имеет параметр типа int, а не типа byte. Тут происходит нетривиальное(TODO: какое?) преобразование - байт лежит в диапазоне [-128,127], а преобразуется в значение int в диапазоне [0, 255].
    Запись всего массива байтов производится методом write(byte[]).
    Запись диапазона из массива байтов производится методом write(byte[],  int, int).Обратите внимание: второй аргумент - это индекс левого конца, но третий аргумент - это длина диапазона, а не индекс правого конца.
    Заметьте, что запись массива и диапазона - безусловны (т.е. либо будут записаны все данные, либо будет инициирована исключительная ситуация - обе ситуации могут произойти после задержки/"залипания"/блокирования метода). Чтение из OutputStream (read(byte[]), read(byte[], int, int)) может не заполнить весь массив данными.


    "Завершающий" методы
    flush() - производит "сбрасывание" данных, если таковые "застряли" в потоке вывода. Основной пример "застревания" - буферизация. Можно вызывать много раз подряд. После вызова поток вывода готов к продолжению записи данных.
    close() - производит "закрытие" потока вывода и освобождения связанных ресурсов. 



   УПРОЩЕННАЯ ВЕРСИЯ:
package java.io;

public class OutputStream {

    public void write(int b) throws IOException;

    public void write(byte b[]) throws IOException;

    public void write(byte b[], int off, int len) throws IOException;

    public void flush() throws IOException {}

    public void close() throws IOException {}
}

    ПОЛНАЯ ВЕРСИЯ:
package java.io;

public abstract class OutputStream implements Closeable, Flushable {

    public abstract void write(int b) throws IOException;

    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

    public void write(byte b[], int off, int len) throws IOException {
        ...
        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }

    public void flush() throws IOException {}

    public void close() throws IOException {}
}


    Читаем побайтно файл через InputStream (FileInputInputStream), пишем побайтно в OutputStream (ByteArrayOutputStream)
import java.io.*;

public class _4_OSTest {
    public static void main(String[] args) throws IOException {
        String fileName = "c:/tmp/text.txt";

        InputStream inFile = null;
        try {
            inFile = new FileInputStream(fileName);
            byte[] data = readFullyByByte(inFile);
            System.out.println(new String(data, "UTF8"));
        } catch (IOException e) {
            throw new IOException("Exception when open+read file " + fileName, e);
        } finally {
            closeQuietly(inFile);
        }
    }

    public static byte[] readFullyByByte(InputStream in) throws IOException {
        int oneByte;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        while ((oneByte = in.read()) != -1) {
            out.write(oneByte);
        }
        return out.toByteArray();
    }

    private static void closeQuietly(InputStream inFile) {
        if (inFile != null) {
            try {
                inFile.close();
            } catch (IOException ignore) {
                /*NOP*/
            }
        }
    }
}


    Читаем массивами файл через InputStream (FileInputInputStream), пишем массивами в OutputStream (ByteArrayOutputStream)
import java.io.*;

public class _5_OSTest_array {
    public static void main(String[] args) throws IOException {
        String fileName = "c:/tmp/text.txt";

        InputStream inFile = null;
        try {
            inFile = new FileInputStream(fileName);
            byte[] data = readFullyByByte(inFile);
            System.out.println(new String(data, "UTF8"));
        } catch (IOException e) {
            throw new IOException("Exception when open and read file " + fileName, e);
        } finally {
            closeQuietly(inFile);
        }
    }

    public static byte[] readFullyByByte(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buff = new byte[5];
        int count;
        while ((count = in.read(buff)) != -1) {
            out.write(buff, 0, count);
        }
        return out.toByteArray();
    }

    private static void closeQuietly(InputStream inFile) {
        if (inFile != null) {
            try {
                inFile.close();
            } catch (IOException ignore) {
                /*NOP*/
            }
        }
    }
}




    Демонстрируем огромную разницу времени побайтного чтения и чтения буферами (у меня "c:/tmp/image0.png" - это картинка 500Кб):    
import java.io.*;

public class _6_OSTest_different_buffers {
    public static void main(String[] args) throws IOException {
        String fileFromName = "c:/tmp/image0.png";
        String fileToName = "c:/tmp/image1.png";

        for (int k = 1; k < 64 * 1024; k *= 2) {
            InputStream in = null;
            OutputStream out = null;
            try {
                in = new FileInputStream(fileFromName);
                out = new FileOutputStream(fileToName);
                long startTime = System.currentTimeMillis();
                copy(in, out, k);
                long stopTime = System.currentTimeMillis();
                System.out.println("Elapsed time = " + (stopTime - startTime));
            } catch (IOException e) {
                throw new IOException("Exception when copy from '" + fileFromName + "' to file '" + fileToName + "'", e);
            } finally {
                closeQuietly(in);
                closeAndFlushQuietly(out);
            }
        }
    }

    public static void copy(InputStream in, OutputStream out, int bufferSize) throws IOException {
        byte[] buff = new byte[bufferSize];
        int count;
        while ((count = in.read(buff)) != -1) {
            out.write(buff, 0, count);
        }
    }

    private static void closeQuietly(InputStream in) {
        if (in != null) {
            try {
                in.close();
            } catch (IOException ignore) {/*NOP*/}
        }
    }

    private static void closeAndFlushQuietly(OutputStream out) {
        if (out != null) {
            try {
                out.flush();
            } catch (IOException ignore) {/*NOP*/}
            try {
                out.close();
            } catch (IOException ignore) {/*NOP*/}
        }
    }
}
>> Elapsed time = 2290
>> Elapsed time = 1201
>> Elapsed time = 1022
>> Elapsed time = 512
>> Elapsed time = 262
>> Elapsed time = 127
>> Elapsed time = 63
>> Elapsed time = 34
>> Elapsed time = 16
>> Elapsed time = 9
>> Elapsed time = 5
>> Elapsed time = 3
>> Elapsed time = 2
>> Elapsed time = 1
>> Elapsed time = 1
>> Elapsed time = 1



    Демонстрируем, что ввод/вывод часто сопряжен с блокирующими операциями (операциями выполняющимися неопределенно долго). Заметьте - время отмеряем в НАНОСЕКУНДАХ, выводим только случаи задержек. Подсказка - данные приходят в виде IP-пакетов, ждем данных пакета долго (миллисекунды), потом очень быстро их вычитываем .потом ждем следующего пакета:
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class _7_ISTest_blocked_operations {
    public static void main(String[] args) throws IOException {
        InputStream is = new URL("http://lenta.ru").openStream();
        long startTime = System.nanoTime();
        while (is.read() != -1) {
            long stopTime = System.nanoTime();
            long dT = stopTime - startTime;
            if (dT > 1000000) {
                System.out.println("Elapsed time = " + dT/1000000 + "ms");
            }
            startTime = stopTime;
        }
    }
}


>> Elapsed time = 33ms
>> Elapsed time = 31ms
>> Elapsed time = 16ms
>> Elapsed time = 16ms
>> Elapsed time = 3ms
>> Elapsed time = 13ms
>> Elapsed time = 3ms
>> Elapsed time = 14ms
>> Elapsed time = 2ms
>> Elapsed time = 14ms
>> Elapsed time = 4ms
>> Elapsed time = 3ms
>> Elapsed time = 2ms
>> Elapsed time = 4ms
>> Elapsed time = 7ms



x

x


x
x
x


x
x
x


x
x
x


x
x
x


x
x
x




    Лабораторные

    io.streams.remove_zero
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class BAOSTest {
    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream buff = new ByteArrayOutputStream();
        buff.write(0);
        buff.write(1);
        buff.write(2);
        byte[] arr = buff.toByteArray();
        System.out.println(Arrays.toString(arr));
    }
}

    Написать программу, которая вычитывает файл и записывает файл, удаляя все байты равные 0.
    а) добавьте в текущую программу корректное завершение работы c потоками(flush(), close()) + гарантированные вызов close() ОБОИХ потоков даже в случае IOException.
    б) добавьте буферизацию (BufferedInputStream + BufferedOutputStream) в чтение из и запись в файл. Сравните время работы без буфера и с буфером размером 1, 2, 4, 8, ..., 1024 байта.
    с) кроме буферизации добавьте чтение в массив (read(byte[]) или read(byte[], int, int)) + запись массивом (write(byte[] или write(byte[], int, int))).
    P.S. Для заметного ускорения проводите тестирование на большом (более 1Мб).