ברוכים הבאים ליחידת הלימוד בנושא "קבצים וזרמי מידע" בקורס "תכנות מתקדם בשפת Java" (20554). יחידה זו חיונית להבנת האופן שבו תוכניות Java מתקשרות עם מערכת הקבצים, קוראות וכותבות נתונים באופן עקבי, ומאפשרות אחסון מידע מעבר לזמן הריצה של התוכנית. נלמד על ייצוג קבצים ותיקיות, עקרונות זרמי הקלט/פלט, שיפור ביצועים באמצעות חיץ (buffering), והיכולת לשמור אובייקטים שלמים לקובץ באמצעות סריאליזציה.
אובייקטי File – ייצוג קבצים ותיקיות
מחלקת java.io.File אינה מייצגת את תוכן הקובץ עצמו, אלא מהווה הפשטה (abstraction) של נתיב במערכת הקבצים. היא מאפשרת לנו לבצע פעולות על קבצים ותיקיות כגון בדיקת קיומם, קבלת מידע עליהם, יצירה, מחיקה ושינוי שמם.
פעולות נפוצות עם אובייקטי File:
- בדיקת קיום:
file.exists() - סוג הנתיב:
file.isFile(),file.isDirectory() - קבלת מידע:
file.getName(),file.getPath(),file.length()(גודל בבתים) - יצירה ומחיקה:
file.createNewFile(),file.mkdir(),file.mkdirs(),file.delete() - רישום תוכן תיקייה:
file.listFiles()
זרמי קלט/פלט (Input/Output Streams) – הבסיס לקריאה וכתיבה
זרמים הם הדרך הסטנדרטית ב-Java לקרוא ולכתוב נתונים. הם מייצגים רצף של נתונים הנעים ממקור ליעד. Java מבחינה בין שני סוגים עיקריים של זרמים:
זרמי בתים (Byte Streams)
מטפלים בנתונים כרצף של בתים (byte). מתאימים לכל סוגי הקבצים (טקסט, תמונות, קבצי הפעלה). המחלקות הבסיסיות הן InputStream ו-OutputStream. דוגמאות קונקרטיות: FileInputStream, FileOutputStream.
זרמי תווים (Character Streams)
מטפלים בנתונים כרצף של תווים (char). מתאימים במיוחד לקבצי טקסט, מכיוון שהם מטפלים באופן אוטומטי בקידוד התווים (לדוגמה, UTF-8). המחלקות הבסיסיות הן Reader ו-Writer. דוגמאות קונקרטיות: FileReader, FileWriter.
טיפול בזרמים:
חשוב לסגור זרמים לאחר השימוש כדי לשחרר משאבי מערכת. הדרך המומלצת לעשות זאת היא באמצעות בלוק try-with-resources, המבטיח סגירה אוטומטית גם במקרה של שגיאה.
try (FileOutputStream fos = new FileOutputStream("output.dat")) {
fos.write(123);
} catch (IOException e) {
e.printStackTrace();
}
זרמים מבוּפרים (Buffered Streams) – יעילות וביצועים
פעולות קלט/פלט הן יקרות מבחינת ביצועים, מכיוון שהן כרוכות באינטראקציה עם חומרת המערכת. זרמים מבוּפרים משפרים את היעילות על ידי צמצום מספר הפעולות הפיזיות של קריאה/כתיבה.
מחלקות נפוצות:
BufferedInputStream,BufferedOutputStream(לזרמי בתים)BufferedReader,BufferedWriter(לזרמי תווים)
זרמים אלו עוטפים זרמים קיימים. לדוגמה, כדי לקרוא קובץ טקסט ביעילות:
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
flush() מאלצת את הזרם לכתוב את כל הנתונים שבחיץ ליעד. קריאה ל-close() סוגרת את הזרם וגם מבצעת flush() באופן אוטומטי. חשוב להבין שאם התוכנית מסתיימת באופן לא צפוי לפני close() או flush(), נתונים עלולים להישאר בחיץ ולא להיכתב לקובץ, מה שיוביל לאובדן מידע. לכן, שימוש ב-try-with-resources הוא קריטי.סריאליזציה (Serialization) – שמירת אובייקטים שלמים
סריאליזציה היא תהליך המרת מצבו של אובייקט לרצף של בתים, כך שניתן יהיה לאחסן אותו בקובץ או להעביר אותו דרך רשת, ולאחר מכן לשחזר אותו לאובייקט זהה.
כיצד לבצע סריאליזציה:
כדי שאובייקט יהיה ניתן לסריאליזציה, מחלקתו חייבת לממש את הממשק java.io.Serializable.
המחלקות העיקריות לביצוע סריאליזציה הן ObjectOutputStream לכתיבה ו-ObjectInputStream לקריאה.
// מחלקה לדוגמה
class MyObject implements Serializable {
private String name;
private int id;
private transient String password; // לא יישמר בסריאליזציה
public MyObject(String name, int id, String password) {
this.name = name;
this.id = id;
this.password = password;
}
// getters, setters, toString...
}
// כתיבת אובייקט לקובץ (סריאליזציה)
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser"))) {
MyObject obj = new MyObject("Alice", 101, "secret");
oos.writeObject(obj);
} catch (IOException e) {
e.printStackTrace();
}
// קריאת אובייקט מקובץ (דסריאליזציה)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.ser"))) {
MyObject restoredObj = (MyObject) ois.readObject();
System.out.println("Restored: " + restoredObj);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
שאלות לדיון
- מתי עדיף להשתמש בזרמי בתים (
InputStream/OutputStream) ומתי בזרמי תווים (Reader/Writer)? תן דוגמאות. - הסבר מדוע שימוש בזרמים מבוּפרים (
Buffered Streams) חיוני לרוב פעולות הקלט/פלט, ומהם הסיכונים באי-שימוש נכון בהם. - כיצד ממשק
Serializableתורם ליכולת של Java לשמור מצב אובייקטים, ומה תפקידה של מילת המפתחtransientבהקשר זה? - מה ההבדל המהותי בין אובייקט
FileלביןFileInputStreamאוFileReader?
נקודות לתשובת מודל
- זרמי בתים מול זרמי תווים: זרמי בתים לנתונים בינאריים (תמונות, קבצי הפעלה, אודיו), זרמי תווים לנתוני טקסט (קבצי קוד, קבצי לוג) תוך טיפול בקידוד.
- זרמים מבוּפרים: משפרים ביצועים על ידי צמצום גישות פיזיות למערכת הקבצים. סיכונים: אי-ביצוע
flush()אוclose()עלול לגרום לאובדן נתונים שנותרו בחיץ.try-with-resourcesפותר זאת. - Serializable ו-transient:
Serializableהוא ממשק סמן המאפשר ל-JVM להמיר אובייקט לזרם בתים.transientמסמן שדות שאין לכלול בתהליך הסריאליזציה, לרוב מטעמי אבטחה (סיסמאות) או ביצועים (נתונים הניתנים לחישוב מחדש). - File מול FileInputStream/FileReader:
Fileמייצג את הנתיב והמאפיינים של קובץ/תיקייה במערכת הקבצים.FileInputStream/FileReaderהם זרמים המשמשים לקריאת תוכן הקובץ עצמו.Fileהוא מטא-דאטה, הזרמים הם הגישה לנתונים.