תכנות מתקדם ב-Java/זרמים

עד כה למדנו להתעסק עם מידע בכמויות שונות - לאחסן אותו, לגשת אליו, ולבצע עליו פעולות שונות. בכל המקרים האלו, המידע עמד מולנו, או שקיבלנו אותו בצורה ברורה ומסודרת. כעת נלמד להתמודד עם דרך חדשה להתעסק במידע - זרמים.

זרמים עריכה

בכמה מקרים, לא ניתן להתייחס אל המידע כאוסף מסודר של נתונים המונח לפנינו, אלא כזרם של מידע המגיע ממקור מסויים, למשל: חבילות המגיעות מהאינטרנט, או מידע הנקרא מקובץ.

במקרים אלו, אופן העבודה שלנו עם המידע (קריאה וכתיבה) שונה מעט מהצורה המוכרת לנו. באופן כללי, עבודה עם מידע המגיע מזרם דורשת קריאה של פיסת מידע ועבודה איתה, בדרך כלל בלולאה הנמשכת כל עוד נקרא מידע חדש.

דוגמה - קריאת קובץ טקסט עריכה

נראה כאן תוכנית פשוטה שקוראת את כל תוכנו של קובץ טקסט (ששמו ניתן כפרמטר לתוכנית) ומדפיסה אותו.

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class TextFileReader {

    public static void main(String[] args) throws IOException {
        String fileName = args[0];
        File f = new File(fileName);
        FileInputStream fi = new FileInputStream(f);
        int ch;
        while((ch = fi.read()) != -1) {
            System.out.print((char) ch);
        }
        fi.close();
    }

}

למען הקריאות והפשטות, התוכנית לא תופסת חריגות ולא בודקת דבר, אך כמובן שבתוכנית שימושית חובה להוסיף טיפול בשגיאות. נעבור כאן על משמעותן של הפקודות העיקריות: File f = new File(fileName); באופן לא מפתיע, פקודה זו יוצרת אובייקט מסוג File, שהוא הכלי הבסיסי בעזרתו מתבצעת כל עבודה עם קבצים. לאובייקט File קיימים כמה בנאים, כאן השתמשנו באחד הפשוטים - בנאי שמקבל מחרוזת שהיא שם קובץ ויוצר אובייקט שמתייחס לאותו הקובץ. FileInputStream fi = new FileInputStream(f); כאן אנו מגיעים לזרם עצמו: האובייקט FileInputStream הוא אובייקט מיוחד שמגלם זרם של מידע שמגיע מקובץ. אחרי שיצרנו את הזרם, אפשר להתחיל לעבוד:

int ch;
while((ch = fi.read()) != -1) {
      System.out.print((char) ch);
}

שורות קוד אלו הן לב התוכנית, כאן מתבצעת הקריאה מהקובץ וההדפסה. נתעמק בהן יותר: ch = fi.read() כאן מתבצעת הקריאה עצמה. מהזרם fi נקרא תו בודד (מהטיפוס int. שימו לב שבהדפסה המרנו אותו לטיפוס char). הקריאה נעשתה כ-int ולא כאוסף של מילים - הזרם בו השתמשנו משמש לקריאה של ביטים, לאו דווקא לקריאה של מילים ומשפטים. while((ch = fi.read()) != -1) משמעות הפקודה הזו היא: קרא כל עוד הקובץ לא נגמר. כאשר הזרם מחזיר "-1", ניתן לדעת שהגענו לסוף הקובץ. fi.close(); לא פחות חשוב מהשאר: כאשר מתעסקים עם זרמים, חובה לסגור אותם בסוף השימוש. אפשר להשאיר זרמים פתוחים - התוכנית תרוץ, אך במקרים רבים זהו מתכון לצרות.

עטיפה של זרמים עריכה

הזרמים הבסיסיים מכילים מספר מצומצם של תכונות: קריאה או כתיבה פשוטה בלבד. כדי לעשות אותם נוחים יותר לעבודה, ניתן לעטוף אותם. למשל: הזרם הבסיסי שראינו, FileInputStream, מאפשר קריאה סדרתית של הקובץ. נניח כעת שאנו רוצים להוסיף לו חוצץ (Buffer) - מעין מערך פנימי בו נאגרים הנתונים שנקראים מהקובץ. למטרה זו, קיימת המחלקה BufferedInputStream, שיכולה לעטוף אובייקט מטיפוס InputStream. העטיפה נעשית בצורה הבאה (בהנחה ש-f הוא אובייקט מטיפוס File שכבר אותחל קודם): BufferedInputStream bfi = new BufferedInputStream(new FileInputStream(f)); כעת האובייקט החדש שיצרנו מהווה מעין "עטיפה" סביב האובייקט המקורי. את הפעולות שנרצה לבצע - נבצע על האובייקט החדש.

זרמים שכדאי להכיר עריכה

הזרמים בג'אווה מתחלקים לזרמים של קריאה, וזרמים של כתיבה. כמו כן, ישנם זרמים המיועדים לקריאה של מידע גולמי, מול זרמים המיועדים לטקסט. בנוסף לזרמים בסיסיים אלו קיימים גם סוגים נוספים שתפקידם להעניק תכונות נוספות לזרמים קיימים באמצעות עטיפתם. נעבור על מספר מצומצם של זרמים.

זרמים המיועדים לטקסט עריכה

קריאה עריכה

המחלקה הבסיסית עבור זרמים המיועדים לקריאת טקסט נקראת Reader. זוהי מחלקה מופשטת, וקיימים עבורה כמה מחלקות שמרחיבות אותה. החשובות שבהן:

  • המחלקה FileReader מיועדת לקריאה של קבצי טקסט.
  • המחלקה StringReader מיועדת לקריאה של מחרוזות כזרם.

כתיבה עריכה

המחלקה הבסיסית עבור זרמים המיועדים לכתיבת טקסט נקראת (כצפוי) Writer. באופן זהה, גם היא מחלקה מופשטת לה קיימות כמה הרחבות אפשריות, ובהן:

  • המחלקה FileWriter מיועדת לכתיבה בקבצי טקסט.
  • המחלקה StringWriter מיועדת לכתיבה למחרוזות.

זרמים המיועדים למידע גולמי עריכה

קריאה עריכה

המחלקה הבסיסית עבור קריאה של מידע גולמי נקראת InputStream, ובדומה ל-Reader, היא מחלקה מופשטת. הרחבות עיקריות שלה:

  • המחלקה FileInputStream מיועדת לקריאת קבצים.

כתיבה עריכה

המחלקה הבסיסית כאן היא OutputStream. הרחבות עיקריות:

  • המחלקה FileOutputStream מיועדת לכתיבת מידע גולמי לקבצים.

שימו לב כי זרמים אלו מסוגלים להתמודד גם עם קבצי טקסט (כמו שאפשר לראות בדוגמה למעלה). עם זאת, מכיוון שהם מיועדים לעבודה עם מידע גולמי כלשהו (כלומר - אוסף של נתונים מטיפוס unsigned char), נוח יותר לעבוד עם הזרמים המיועדים לטקסט.

זרמים נוספים עריכה

  • עבור כל סוגי הזרמים הבסיסיים שראינו, קיים זרם עוטף המצייד אותו בחוצץ זמני להחזקת הנתונים, כלומר - בכל פעם החוצץ הזמני קורא כמות מסויימת של נתונים ואוגר אותה. שמות זרמים אלו מתחילים תמיד במילה Buffered. למשל: BufferedInputStream, או BufferedWriter.

עבודה עם קידודים מיוחדים עריכה

כדי לכתוב (ולקרוא) בקידודים מיוחדים, כמו למשל - קידוד UTF8, יש להשתמש בזרם מסוג InputStreamReader (לקריאה) או OutputStreamWriter (לכתיבה), ולהגדיר את סוג הקידוד הרצוי. אלו הם זרמים שעוטפים זרמים קיימים, למשל - זרם קריאה מקובץ. נראה כאן דוגמה לפתיחת זרמים עבור קריאה וכתיבה מקובץ, כאשר הקידוד הרצוי הוא UTF8. אחרי יצירת הזרמים האלו אנו עוטפים אותם בזרמים מטיפוס BufferedReader ו-BufferedWriter, איתם נוח יותר לעבוד.

// Write to file.txt

// Open the file
File f1 = new File("file.txt");
// Open a file writer object
FileOutputStream fos = new FileOutputStream(f);
// Wrap the file writer with OutputStreamWriter with UTF8 encoding
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF8");
// Wrap it with buffered writer
BufferedWriter writer = new BufferedWriter(osw);

// Read from file.txt

// Open the file
File f2 = new File("file.txt");
// Open a file reader object
FileInputStream fis = new FileInputStream(f2);
// Wrap the file reader with InputStreamReader with UTF8 encoding
InputStreamReader isr = new InputStreamReader(fis, "UTF8");
// Wrap it with buffered reader
BufferedReader reader = new BufferedReader(isr);

יש כאן זרמים רבים שעוטפים זה את זה, מה שמביא לקוד מעט מסורבל. כדי ליצור קוד קצר יותר, אפשר לכתוב את הכל יחד, באופן הבא:

// Write to file, UTF8
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File("file.txt"), "UTF8")));
// Read from file, UTF8
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File("file.txt"), "UTF8")));

דוגמה עריכה

נראה דוגמה פשוטה של תוכנית הקוראת קובץ טקסט, מדפיסה את תוכנו, ובנוסף מעתיקה אותו לקובץ טקסט אחר (שמות הקבצים מתקבלים כארגומנטים משורת הפקודה)

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**
 * This class copies the contents of a text file into anoter text file
 */
public class Main {

    /**
     * @param args Input file and output file names
     */
    public static void main(String[] args) {
        // The file read
        File in = new File(args[0]);
        // The target file
        File out = new File(args[1]);
        FileReader fr = null;
        FileWriter fw = null;
        // Try block: Most stream operations may throw IO exception
        try {
            // Create file reader and file writer objects
            fr = new FileReader(in);
            fw = new FileWriter(out);
            // Wrap the reader and the writer with buffered streams
            BufferedReader reader = new BufferedReader(fr);
            BufferedWriter writer = new BufferedWriter(fw);
            String line;
            while ((line = reader.readLine()) != null) {
                // Print the line read and write it to the output file
                System.out.println(line);
                writer.write(line + "\n");
            }
            // Close the streams
            reader.close();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(0);
        }
    }
}


הפרק הקודם:
ביטויי למבדה
זרמים הפרק הבא:
עבודה עם קבצים