Stäng efter dig när du går
- tor 22 apr, 2010 kl 16:39
- 8 kommentarer
- Java, Operativsystem
Java är ett snällt språk som tar hand om skräpsamling för oss. Vi behöver inte tänka på att avallokera minnet efter vi använt det.. jättesmidigt! Dock verkar det som att denna automatiska skräpuppsamling har gjort oss lata och glömmer bort att vi faktiskt måste städa upp efter oss ibland!
Strömmar av alla olika slag är nämligen väldigt viktiga att stänga. Framförallt strömmar till filer är viktiga, eftersom en öppen ström mot en fil tar upp systemresurser. Om man inte stänger filströmmarna efter sig, kommer ditt Java-program äta upp systemresurser tills minnet tar slut, eller tills operativsystemet sätter stopp för att du har öppnat för många filer (känns felmeddelandet ”Too many open files” igen?). Om du kör ditt program på en Linuxmaskin räknas både filer och TCP-anslutningar som filkopplingar. Detta gör att om du öppnar för många filer på disk, så kommer det heller inte att gå att öppna nya sockets.
Det finns många sätt att stänga sin ström på, men vad är då det mest korrekta sättet?
Det vanligaste jag sett är följande:
public void writeToFile(TextProvider textProvider) {
try {
FileWriter fw = new FileWriter("fil.txt");
fw.write(textProvider.getText());
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Om anropet till TextProvider skulle kasta ett exception skulle aldrig strömmen stängas. Exekveringen av try-blocket skulle avslutas och koden i catch-blocket skulle då köras med en stacktrace och en öppen fil som följd.
För att stänga strömmen på rätt sätt måste du istället göra såhär:
public void writeToFile(TextProvider textProvider) {
FileWriter fw = null;
try {
fw = new FileWriter("fil.txt");
fw.write(textProvider.getText());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e){
// Ignore
}
}
}
}
Följande regler gäller:
2. Tilldela ström-variabeln i try-blocket
3. Utför din(a) I/O operation(er) på strömmen
4. Stäng inte strömmen i try-blocket, utan i finally-blocket
5. Se till att stängningen sker på ett säkert sätt
Det viktiga är att man stänger strömmen i finally-blocket. Den kod som finns i finally-blocket kommer alltid anropas, oavsett om något exception kastas i try-blocket eller inte. Om anropet till TextProvider skulle gå fel här, skulle exekveringen av try-blocket avslutas, koden i catch-blocket köras och sist (men inte minst) kommer finally-blocket exekveras där strömmen stängs. Stängningen sker här på ett säkert sätt, med först en null-kontroll (ifall aldrig tilldelning av ström-variabeln skedde) och sedan en try-catch runt close-anropet för att garantera att inget exception kastas vidare.
Det blir dock rätt jobbigt att alltid skriva dessa rader i finally-blocket på alla ställen där man stänger strömmar. För att slippa skriva den långa harangen kod i finally-blocket varje gång, brukar jag göra en metod av det. Följande kodsnutt är lite bättre:
public void writeToFile(TextProvider textProvider) {
FileWriter fw = null;
try {
fw = new FileWriter("fil.txt");
fw.write(textProvider.getText());
} catch (Exception e) {
e.printStackTrace();
} finally {
close(fw);
}
}
private void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e){
// Ignore
}
}
}
Om man vill kan man skriva close-metoden på detta sätt också för att slippa null-kontrollen:
private void close(Closeable closeable) {
try {
closeable.close();
} catch (Exception e){
// Ignore
}
}
Jag brukar bryta ut denna metod till en egen klass vid namn IOUtils, eller liknande. Om man använder Apache Commons i sitt projekt finns redan metoder för att stänga strömmar i en klass med just detta namn (IOUtils under paketet org.apache.commons.io).
Happy closing!
Jag tycker det här är värst när det gäller Java, det är så förbannat lätt att göra fel och resultaten kan bli ödesdigra. C#s using operator är suverän som tar hand om detta åt mig som programmera. Kod som ovan är bloatcode av den värsta sorten.
Tydligen skall JDK 7 komma med liknande funktionalitet, men den skulle vi ha haft för fem år sedan.
Jag tycker nog fortfarande att det är alldeles för mycket kod som dupliceras… Skriv en template som abstraherar bort all plumbing-kod som står mellan dig och det du egentligen vill åstadkomma. Kodduplicering är elakt.
new WriteToFileTemplate("fil.txt"){
void write(FileWriter fw) throws IOException {
fw.write(textProvider.getText());
}
}.execute();
Och impl.
abstract class WriteToFileTemplate {
private final String fileName;
public WriteToFileTemplate(String fileName) {
this.fileName = fileName;
}
protected void execute() {
FileWriter fw = null;
try {
fw = new FileWriter(fileName);
write(fw);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
// Ignore
}
}
}
}
abstract void write(FileWriter fw) throws IOException;
}
vilken härlig formatering det blev :)
Visst saknar man closures i java ;)
@redsolo: Jag kan inte mer än hålla med..! Det är oerhört lätt att göra fel och koden blir inte alls snygg. Just nu är det väl det vi får hålla oss till..
Du har rätt i att det kommer bättring! I JDK7 kommer man kunna deklarera sin variabel till strömmen i en parameter till try-satsen. Typ såhär:
try (BufferedReader br = new BufferedReader(new FileReader(path)) {
return br.readLine();
}
Det som deklareras i try-parameterlistan måste implementera Closeable och sen sköter den själv anropet till close-metoden transparent.
@Peter: Snygg lösning! Visst hade det blivit lite smidigare om man hade haft closures.. men den som väntar på något gott! ;)
Samma sak för databaskopplingar. Stäng dem annars tar de slut.
Har man inte tid nu att göra det på ett snyggt så vänta på Java 7 då resurser stängs automatiskt.
Fast då visar sig väl latheten på nåt annat :)
@Pierre: Lösningen är inte bara snygg utan även dokumenterad på döda träd (papper) sedan 1994 (ISBN:0201633612 bla.) och på ”Portland Pattern Repository” (http://c2.com/cgi/wiki) sedan urminnes tider…
Python hanterar detta fint också, så jython är ju ett alternativ :D