Wednesday, September 27, 2006

Debug JSF lifecycle

Listen and debug JSF lifecycle phases

The JSF lifecycle will be explained and debugged here using the "poor man's debugging" approach with sysout's. We'll also check what happens if you add immediate="true" to the UIInput and UICommand and what happens when a ConverterException and ValidatorException will be thrown.

Well, you probably already know that the JSF lifecycle contains 6 phases:

  1. Restore view
  2. Apply request values
  3. Process validations
  4. Update model values
  5. Invoke application
  6. Render response

You can use a PhaseListener to trace the phases of the JSF lifecycle and execute some processes where required. But you can also use a "dummy" PhaseListener to debug the phases to see what is happening in which phase. Here is a basic example of such a LifeCycleListener:

Note: if you don't have a JSF playground environment setup yet, then you may find this tutorial useful as well: JSF tutorial with Eclipse and Tomcat.

package mypackage;

import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;

public class LifeCycleListener implements PhaseListener {

    public PhaseId getPhaseId() {
        return PhaseId.ANY_PHASE;
    }

    public void beforePhase(PhaseEvent event) {
        System.out.println("START PHASE " + event.getPhaseId());
    }

    public void afterPhase(PhaseEvent event) {
        System.out.println("END PHASE " + event.getPhaseId());
    }

}

Add the following lines to the faces-config.xml to activate the LifeCycleListener.

<lifecycle>
    <phase-listener>mypackage.LifeCycleListener</phase-listener>
</lifecycle>

This produces like the following in the system output:

START PHASE RESTORE_VIEW 1
END PHASE RESTORE_VIEW 1
START PHASE APPLY_REQUEST_VALUES 2
END PHASE APPLY_REQUEST_VALUES 2
START PHASE PROCESS_VALIDATIONS 3
END PHASE PROCESS_VALIDATIONS 3
START PHASE UPDATE_MODEL_VALUES 4
END PHASE UPDATE_MODEL_VALUES 4
START PHASE INVOKE_APPLICATION 5
END PHASE INVOKE_APPLICATION 5
START PHASE RENDER_RESPONSE 6
END PHASE RENDER_RESPONSE 6

Back to top

Basic debug example

To trace all phases of the JSF lifecycle, here is some sample code which represents simple JSF form with a "dummy" converter and validator and the appropriate backing bean with all component bindings. The code sample can be used to give us more insights into the phases of the JSF lifecycle, to understand it and to learn about it. Please note that component bindings are rarely used in real world this way. They are in this code sample just there to track and log component set/get actions by JSF. In real world code you should not have the need for them.

Here's the form, just put it in its entirety in the body of your test.jsp file (note: at time of writing, JSF 1.2 is used, but if you're using JSF 2.x, you can of course also put this in a test.xhtml file):

<h:form>
    <h:inputText
        binding="#{myBean.inputComponent}"
        value="#{myBean.inputValue}"
        valueChangeListener="#{myBean.inputChanged}">
        <f:converter converterId="myConverter" />
        <f:validator validatorId="myValidator" />
    </h:inputText>
    <h:commandButton
        value="submit"
        action="#{myBean.action}" />
    <h:outputText
        binding="#{myBean.outputComponent}"
        value="#{myBean.outputValue}" />
    <h:messages />
</h:form>

Here's the dummy converter: MyConverter.java

package mypackage;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

public class MyConverter implements Converter {

    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        System.out.println("MyConverter getAsObject: " + value);
        return value;
    }

    public String getAsString(FacesContext context, UIComponent component, Object value) {
        System.out.println("MyConverter getAsString: " + value);
        return (String) value;
    }

}

Here's the dummy validator: MyValidator.java

package mypackage;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

public class MyValidator implements Validator {

    public void validate(FacesContext context, UIComponent component, Object value)
        throws ValidatorException
    {
        System.out.println("MyValidator validate: " + value);
    }

}

Here's the backing bean: MyBean.java

package mypackage;

import javax.faces.component.html.HtmlInputText;
import javax.faces.component.html.HtmlOutputText;
import javax.faces.event.ValueChangeEvent;

public class MyBean {

    // Init ----------------------------------------------------------------------------------------

    private HtmlInputText inputComponent;
    private String inputValue;
    private HtmlOutputText outputComponent;
    private String outputValue;

    // Constructors -------------------------------------------------------------------------------

    public MyBean() {
        log("constructed");
    }

    // Actions ------------------------------------------------------------------------------------

    public void inputChanged(ValueChangeEvent event) {
        log(event.getOldValue() + " to " + event.getNewValue());
    }

    public void action() {
        outputValue = inputValue;
        log("succes");
    }

    // Getters/setters ----------------------------------------------------------------------------

    public HtmlInputText getInputComponent() {
        log(inputComponent);
        return inputComponent;
    }

    public void setInputComponent(HtmlInputText inputComponent) {
        log(inputComponent);
        this.inputComponent = inputComponent;
    }

    public String getInputValue() {
        log(inputValue);
        return inputValue;
    }

    public void setInputValue(String inputValue) {
        log(inputValue);
        this.inputValue = inputValue;
    }

    public HtmlOutputText getOutputComponent() {
        log(outputComponent);
        return outputComponent;
    }

    public void setOutputComponent(HtmlOutputText outputComponent) {
        log(outputComponent);
        this.outputComponent = outputComponent;
    }

    public String getOutputValue() {
        log(outputValue);
        return outputValue;
    }

    // Helpers ------------------------------------------------------------------------------------

    private void log(Object object) {
        String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();
        System.out.println("MyBean " + methodName + ": " + object);
    }
}

The minimal faces configuration: faces-config.xml

<converter>
    <converter-id>myConverter</converter-id>
    <converter-class>mypackage.MyConverter</converter-class>
</converter>
<validator>
    <validator-id>myValidator</validator-id>
    <validator-class>mypackage.MyValidator</validator-class>
</validator>
<managed-bean>
    <managed-bean-name>myBean</managed-bean-name>
    <managed-bean-class>mypackage.MyBean</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
</managed-bean>

Note: at the time of writing, JSF 1.2 was the standard and JSF 2.0 didn't exist yet. If you're however using JSF 2.x, then you could of course also instead use the @FacesConverter("myConverter"), @FacesValidator("myValidator") and @ManagedBean @RequestScoped annotations respectively.

Back to top

The first call

The first call in a freshly started webapplication with a fresh session should output at least:

START PHASE RESTORE_VIEW 1
END PHASE RESTORE_VIEW 1
START PHASE RENDER_RESPONSE 6
MyBean <init>: constructed
MyBean getInputComponent: null
MyBean setInputComponent: javax.faces.component.html.HtmlInputText@2a9fca57
MyBean getInputValue: null
MyBean getOutputComponent: null
MyBean setOutputComponent: javax.faces.component.html.HtmlOutputText@13bbca56
MyBean getOutputValue: null
END PHASE RENDER_RESPONSE 6

1. Restore view.
As the session is fresh, there's no means of any UIViewRoot to restore, so nothing to see here.

2. Apply request values.
This phase is skipped because there is no form submit.

3. Process validations.
This phase is skipped because there is no form submit.

4. Update model values.
This phase is skipped because there is no form submit.

5. Invoke application.
This phase is skipped because there is no form submit.

6. Render response.
The bean is constructed. Behind the scenes a new UIViewRoot is created and stored in the session. If the component binding getters returns precreated components (precreated in e.g. the constructor) and not null, then those will be used, otherwise JSF will create new components. The components will be stored in the UIViewRoot and the bounded components are set in the component bindings. The values to be shown are retrieved from the value binding getters in the backing bean. If the values aren't set yet, they defaults to null. The component bindings are not required by the way. Only use them if you actually need the component in the backing bean for other means than getting/setting the value. In this article they are included just to demonstrate what all happens in the lifecycle.

Back to top

The form submit

The form submit with the value "test" entered should output at least:

START PHASE RESTORE_VIEW 1
MyBean <init>: constructed
MyBean setInputComponent: javax.faces.component.html.HtmlInputText@2a9fca57
MyBean setOutputComponent: javax.faces.component.html.HtmlOutputText@13bbca56
END PHASE RESTORE_VIEW 1
START PHASE APPLY_REQUEST_VALUES 2
END PHASE APPLY_REQUEST_VALUES 2
START PHASE PROCESS_VALIDATIONS 3
MyConverter getAsObject: test
MyValidator validate: test
MyBean getInputValue: null
MyBean inputChanged: null to test
END PHASE PROCESS_VALIDATIONS 3
START PHASE UPDATE_MODEL_VALUES 4
MyBean setInputValue: test
END PHASE UPDATE_MODEL_VALUES 4
START PHASE INVOKE_APPLICATION 5
MyBean action: succes
END PHASE INVOKE_APPLICATION 5
START PHASE RENDER_RESPONSE 6
MyBean getInputValue: test
MyConverter getAsString: test
MyBean getOutputValue: test
END PHASE RENDER_RESPONSE 6

1. Restore view.
The bean is constructed. The UIViewRoot is restored from session and the bounded components are set in the component bindings.

2. Apply request values.
Nothing to see here. Behind the scenes the submitted form values are obtained as request parameters and set in the relevant components in the UIViewRoot, for example inputComponent.setSubmittedValue("test").

3. Process validations.
The submitted values are passed through the converter getAsObject() method and validated by the validator. If the conversion and validation succeeds, then the initial input value will be retrieved from the value binding getter and behind the scenes the inputComponent.setValue(submittedValue) and inputComponent.setSubmittedValue(null) will be executed. If the retrieved initial input value differs from the submitted value, then the valueChangeListener method will be invoked.

4. Update model values.
The converted and validated values will now be set in the value binding setters of the backing bean. E.g. myBean.setInputValue(inputComponent.getValue()).

5. Invoke application.
The real processing of the form submission happens here.

6. Render response.
The values to be shown are retrieved from the value binding getters in the backing bean. If a converter is definied, then the value will be passed through the converter getAsString() method and the result will be shown in the form.

Back to top

Add immediate="true" to UIInput only

Extend the h:inputText in the test.jsp with immediate="true":

    ...
    <h:inputText
        binding="#{myBean.inputComponent}"
        value="#{myBean.inputValue}"
        valueChangeListener="#{myBean.inputChanged}"
        immediate="true">
    ...

The form submit with the value "test" entered should output at least:

START PHASE RESTORE_VIEW 1
MyBean <init>: constructed
MyBean setInputComponent: javax.faces.component.html.HtmlInputText@2a9fca57
MyBean setOutputComponent: javax.faces.component.html.HtmlOutputText@13bbca56
END PHASE RESTORE_VIEW 1
START PHASE APPLY_REQUEST_VALUES 2
MyConverter getAsObject: test
MyValidator validate: test
MyBean getInputValue: null
MyBean inputChanged: null to test
END PHASE APPLY_REQUEST_VALUES 2
START PHASE PROCESS_VALIDATIONS 3
END PHASE PROCESS_VALIDATIONS 3
START PHASE UPDATE_MODEL_VALUES 4
MyBean setInputValue: test
END PHASE UPDATE_MODEL_VALUES 4
START PHASE INVOKE_APPLICATION 5
MyBean action: succes
END PHASE INVOKE_APPLICATION 5
START PHASE RENDER_RESPONSE 6
MyBean getInputValue: test
MyConverter getAsString: test
MyBean getOutputValue: test
END PHASE RENDER_RESPONSE 6

1. Restore view.
The bean is constructed. The UIViewRoot is restored from session and the bounded components are set in the component bindings.

2. Apply request values.
Behind the scenes the submitted form values are obtained as request parameters and set in the relevant components in the UIViewRoot, for example inputComponent.setSubmittedValue("test"). The submitted values are immediately passed through the converter getAsObject() method and validated by the validator. If the conversion and validation succeeds, then the initial input value will be retrieved from the value binding getter and behind the scenes the inputComponent.setValue(submittedValue) and inputComponent.setSubmittedValue(null) will be executed. If the retrieved initial input value differs from the submitted value, then the valueChangeListener method will be invoked. This all happens in this phase instead of the Process validations phase due to the immediate="true" in the h:inputText.

3. Process validations.
Nothing to see here. The conversion and validation is already processed in the Apply request values phase, before the values being put in the components. This is due to the immediate="true" in the h:inputText.

4. Update model values.
The converted and validated values will now be set in the value binding setters of the backing bean. E.g. myBean.setInputValue(inputComponent.getValue()).

5. Invoke application.
The real processing of the form submission happens here.

6. Render response.
The values to be shown are retrieved from the value binding getters in the backing bean. If a converter is definied, then the value will be passed through the converter getAsString() method and the result will be shown in the form.

Note for other components without immediate: any other UIInput components inside the same form which don't have immediate="true" set will just continue the lifecycle as usual.

Back to top

Add immediate="true" to UICommand only

Extend the h:commandButton in the test.jsp with immediate="true" (don't forget to remove from h:inputText):

    ...
    <h:commandButton
        value="submit"
        action="#{myBean.action}"
        immediate="true" />
    ...

The form submit with the value "test" entered should output at least:

START PHASE RESTORE_VIEW 1
MyBean <init>: constructed
MyBean setInputComponent: javax.faces.component.html.HtmlInputText@2a9fca57
MyBean setOutputComponent: javax.faces.component.html.HtmlOutputText@13bbca56
END PHASE RESTORE_VIEW 1
START PHASE APPLY_REQUEST_VALUES 2
MyBean action: succes
END PHASE APPLY_REQUEST_VALUES 2
START PHASE RENDER_RESPONSE 6
MyBean getOutputValue: null
END PHASE RENDER_RESPONSE 6

1. Restore view.
The bean is constructed. The UIViewRoot is restored from session and the bounded components are set in the component bindings.

2. Apply request values.
The real processing of the form submission happens here. This happens in this phase instead of the Invoke application phase due to the immediate="true" in the h:commandButton. The UIInput components which don't have immediate="true" set will not be converted, validated nor updated, but behind the scenes the inputComponent.setSubmittedValue(submittedValue) will be executed before the action() method will be executed.

3. Process validations.
This phase is skipped due to the immediate="true" in the h:commandButton.

4. Update model values.
This phase is skipped due to the immediate="true" in the h:commandButton.

5. Invoke application.
This phase is skipped due to the immediate="true" in the h:commandButton.

6. Render response.
The values to be shown are retrieved from the value binding getters in the backing bean, except for the UIInput components which don't have immediate="true" set. Behind the scenes those will be retrieved from the components in the UIViewRoot, e.g. inputComponent.getSubmittedValue().

Note for all components without immediate: as the Update model values phase is skipped, the value bindings aren't been set and the value binding getters will return null. But the values are still available as submitted value of the relevant components in the UIViewRoot. In this case you can retrieve the non-converted and non-validated input value from inputComponent.getSubmittedValue() in the action() method. You could even change it using inputComponent.setSubmittedValue(newValue) in the action() method.

Back to top

Add immediate="true" to UIInput and UICommand

Extend the h:inputText as well as the h:commandButton in the test.jsp with immediate="true":

    ...
    <h:inputText
        binding="#{myBean.inputComponent}"
        value="#{myBean.inputValue}"
        valueChangeListener="#{myBean.inputChanged}"
        immediate="true">
    ...
    <h:commandButton
        value="submit"
        action="#{myBean.action}"
        immediate="true" />
    ...

The form submit with the value "test" entered should output at least:

START PHASE RESTORE_VIEW 1
MyBean <init>: constructed
MyBean setInputComponent: javax.faces.component.html.HtmlInputText@2a9fca57
MyBean setOutputComponent: javax.faces.component.html.HtmlOutputText@13bbca56
END PHASE RESTORE_VIEW 1
START PHASE APPLY_REQUEST_VALUES 2
MyConverter getAsObject: test
MyValidator validate: test
MyBean getInputValue: null
MyBean inputChanged: null to test
MyBean action: succes
END PHASE APPLY_REQUEST_VALUES 2
START PHASE RENDER_RESPONSE 6
MyConverter getAsString: test
MyBean getOutputValue: null
END PHASE RENDER_RESPONSE 6

1. Restore view.
The bean is constructed. The UIViewRoot is restored from session and the bounded components are set in the component bindings.

2. Apply request values.
Behind the scenes the submitted form values are obtained as request parameters and set in the relevant components in the UIViewRoot, for example inputComponent.setSubmittedValue("test"). The submitted values are immediately passed through the converter getAsObject() method and validated by the validator. If the conversion and validation succeeds, then the initial input value will be retrieved from the value binding getter and behind the scenes the inputComponent.setValue(submittedValue) and inputComponent.setSubmittedValue(null) will be executed. If the retrieved initial input value differs from the submitted value, then the valueChangeListener method will be invoked. This all happens in this phase instead of the Process validations phase due to the immediate="true" in the h:inputText. Finally the real processing of the form submission happens here. This happens in this phase instead of the Invoke application phase due to the immediate="true" in the h:commandButton.

3. Process validations.
This phase is skipped due to the immediate="true" in the h:commandButton.

4. Update model values.
This phase is skipped due to the immediate="true" in the h:commandButton.

5. Invoke application.
This phase is skipped due to the immediate="true" in the h:commandButton.

6. Render response.
The values to be shown are retrieved from the value binding getters in the backing bean. If a converter is definied, then the value will be passed through the converter getAsString() method and the result will be shown in the form.

Note for all components with immediate: as the Update model values phase is skipped, the value bindings aren't been set and the value binding getters will return null. But the values are still available by the relevant components in the UIViewRoot. In this case you can retrieve the input value from inputComponent.getValue() in the action() method. The new input value is also available by the ValueChangeEvent in the inputChanged() method. You could even change it using inputComponent.setValue(newValue) in the action() method.

Note for other components without immediate: any other UIInput components inside the same form which don't have immediate="true" set will not be converted, validated nor updated, but behind the scenes the inputComponent.setSubmittedValue(submittedValue) will be executed before the action() method will be executed. You can retrieve the non-converted and non-validated input value from inputComponent.getSubmittedValue() in the action() method. You could even change it using inputComponent.setSubmittedValue(newValue) in the action() method.

Back to top

Conversion error

Let's see what happens if a conversion error will occur. Change the getAsObject() method of MyConverter.java as follows (and remove the immediate="true" from the test.jsp file):

package mypackage;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;

public class MyConverter implements Converter {

    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        System.out.println("MyConverter getAsObject: " + value);
        throw new ConverterException("Conversion failed.");
    }

    ...
}

The form submit with the value "test" entered should output at least:

START PHASE RESTORE_VIEW 1
MyBean <init>: constructed
MyBean setInputComponent: javax.faces.component.html.HtmlInputText@2a9fca57
MyBean setOutputComponent: javax.faces.component.html.HtmlOutputText@13bbca56
END PHASE RESTORE_VIEW 1
START PHASE APPLY_REQUEST_VALUES 2
END PHASE APPLY_REQUEST_VALUES 2
START PHASE PROCESS_VALIDATIONS 3
MyConverter getAsObject: test
END PHASE PROCESS_VALIDATIONS 3
START PHASE RENDER_RESPONSE 6
MyBean getOutputValue: null
END PHASE RENDER_RESPONSE 6

1. Restore view.
The bean is constructed. The UIViewRoot is restored from session and the bounded components are set in the component bindings.

2. Apply request values.
Nothing to see here. Behind the scenes the submitted form values are obtained as request parameters and set in the relevant components in the UIViewRoot, for example inputComponent.setSubmittedValue("test").

3. Process validations.
The submitted values are passed through the converter getAsObject() method, where a ConverterException is thrown. The validator and the valueChangeListener are bypassed. Also the inputComponent.setValue(submittedValue) won't take place. The lifecycle will proceed to the Render response phase immediately.

4. Update model values.
This phase is skipped due to the ConverterException.

5. Invoke application.
This phase is skipped due to the ConverterException.

6. Render response.
The values to be shown are retrieved from the value binding getters in the backing bean, expect of the values for which a ConverterException has occurred. Behind the scenes those will be retrieved from the components in the UIViewRoot, e.g. inputComponent.getSubmittedValue().

Note: any other UIInput components inside the same form which didn't throw a ConverterException will also skip the update model values and invoke application phases.

Back to top

Validation error

Let's see what happens if a validation error will occur. Change the validate() method of MyValidator.java as follows (and remove the immediate="true" from the test.jsp file and revert MyConverter.java back to original):

package mypackage;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

public class MyValidator implements Validator {

    public void validate(FacesContext context, UIComponent component, Object value)
        throws ValidatorException
    {
        System.out.println("MyValidator validate: " + value);
        throw new ValidatorException(new FacesMessage("Validation failed."));
    }

}

The form submit with the value "test" entered should output at least:

START PHASE RESTORE_VIEW 1
MyBean <init>: constructed
MyBean setInputComponent: javax.faces.component.html.HtmlInputText@2a9fca57
MyBean setOutputComponent: javax.faces.component.html.HtmlOutputText@13bbca56
END PHASE RESTORE_VIEW 1
START PHASE APPLY_REQUEST_VALUES 2
END PHASE APPLY_REQUEST_VALUES 2
START PHASE PROCESS_VALIDATIONS 3
MyConverter getAsObject: test
MyValidator validate: test
END PHASE PROCESS_VALIDATIONS 3
START PHASE RENDER_RESPONSE 6
MyBean getOutputValue: null
END PHASE RENDER_RESPONSE 6

1. Restore view.
The bean is constructed. The UIViewRoot is restored from session and the bounded components are set in the component bindings.

2. Apply request values.
Nothing to see here. Behind the scenes the submitted form values are obtained as request parameters and set in the relevant components in the UIViewRoot, for example inputComponent.setSubmittedValue("test").

3. Process validations.
The values are retrieved as objects from the components, passed through the converter getAsObject() method and validated by the validator, where a ValidatorException is thrown. The valueChangeListener is bypassed. Also the inputComponent.setValue(submittedValue) won't take place. The lifecycle will proceed to the Render response phase immediately.

4. Update model values.
This phase is skipped due to the ValidatorException.

5. Invoke application.
This phase is skipped due to the ValidatorException.

6. Render response.
The values to be shown are retrieved from the value binding getters in the backing bean, except of the values for which a ValidatorException has occurred. Behind the scenes those will be retrieved from the components in the UIViewRoot, e.g. inputComponent.getSubmittedValue().

Note: any other UIInput components inside the same form which didn't throw a ValidatorException will also skip the update model values and invoke application phases.

Back to top

Okay, when should I use the immediate attribute?

If it isn't entirely clear yet, here's a summary, complete with real world use examples when they may be beneficial:

  • If set in UIInput(s) only, the job which is normally invoked during process validations phase will be taken place in apply request values phase instead. Use this to prioritize validation for the UIInput component(s) in question. When validation/conversion fails for any of them, the non-immediate components won't be validated/converted.
  • If set in UICommand only, the apply request values phase until with update model values phases will be skipped for any of the UIInput component(s). Use this to skip the entire processing of the form. E.g. "Cancel" or "Back" button.
  • If set in both UIInput and UICommand components, the job which is normally invoked during the apply request values phase until with update model values phases will be skipped for any of the UIInput component(s) which does not have this attribute set. Use this to skip the processing of the entire form except for certain fields (with immediate). E.g. "Password forgotten" button in a login form with a required and immediate username field and a required but non-immediate password field.
Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) September 2006, BalusC

Monday, June 26, 2006

Bestandsbewerkingen (Dutch)

Introductie

Bij bestandsbewerkingen draait het allemaal om Input/Output datastromen (I/O streams). Met de I/O streams kun je data van een bestand, van het geheugen of van een socket (bijvoorbeeld via een URL) aflezen en/of ernaar schrijven. De klassen die hierbij zeer van nut kunnen zijn zitten in het java.io package. Hierin zullen we alleen de belangrijkste klassen behandelen die van nut kunnen zijn bij het lezen en schrijven van bestanden en directories.

java.io klasse beschrijving
File Definieert bestanden of directories.
FileReader Leest bestand uit als een char array.
FileWriter Schrijft een char array of een String naar een bestand.
BufferedReader Zet een Reader in een geheugenbuffer, dit is sneller bij grotere bestanden (>100KB).
BufferedWriter Zet een Writer in een geheugenbuffer, dit is sneller bij grotere bestanden (>100KB).
CharArrayReader Zet een ordinaire char array in een Reader.
CharArrayWriter Zet een Writer in een ordinaire char array.
FileInputStream Leest bestand uit als een byte array.
FileOutputStream Schrijft een byte array naar een bestand.
BufferedInputStream Zet een InputStream in een geheugenbuffer, dit is sneller bij grotere bestanden (>100KB).
BufferedOutputStream Zet een OutputStream in een geheugenbuffer, dit is sneller bij grotere bestanden (>100KB).
ByteArrayInputStream Zet een ordinaire byte array in een InputStream.
ByteArrayOutputStream Zet een OutputStream in een ordinaire byte array.
InputStreamReader Converteert een byte array naar een char array.
OutputStreamWriter Converteert een char array naar een byte array.
Terug naar boven

Welke moet ik gebruiken?

Dat is vrij simpel: je kunt het beste een Reader en Writer gebruiken voor platte tekstbestanden die uit Latijnse karakters bestaan (txt, csv, ini, etc.) en je kunt het beste een InputStream en OutputStream gebruiken voor binaire bestanden (exe, zip, pdf, tekstbestanden met encoded karakters, etc.). Je mag ze ook door elkaar gebruiken, maar voor de nabewerking is het het handigst wanneer het soort inhoud overeenkomt met de type stream (char array of byte array). Daarnaast biedt de BufferedReader de handige mogelijkheid om platte tekstbestanden regel voor regel te inlezen. Voor de rest werken ze allebei bijna precies hetzelfde.

Terug naar boven

Foutenafhandeling

Praktisch alle klassen van de java.io package kunnen een IOException of een subklasse daarvan afwerpen. Je bent dus verplicht om de I/O bewerkingen in een try-catch-finally statement te zetten. Anders moet je desbetreffende IOException doorgooien via de throws bepaling van de methode. Het is zeer aanbevelenswaardig om de I/O streams na het gebruik van de read() en write() acties altijd te sluiten met de close() methode om de systeembronnen te vrijgeven. Ze worden namelijk niet automatisch direct gesloten. Als je dat niet doet, dan kun je wanneer je veel I/O streams maakt na een tijdje een tekort aan systeembronnen krijgen, met alle desastreuze gevolgen van dien.

Terug naar boven

Werken op bestandsniveau

Met de File kun je een locatie aanwijzen waarvan gelezen moet worden of waarnaar geschreven moet worden. Wanneer je de locatie eenmaal hebt opgegeven, dit kan een bestand of een directory zijn, dan kun je diverse methoden gebruiken om op bestandsniveau te kunnen werken.

Hieronder staat een aardig stukje code om te laten zien wat je allemaal op bestandsniveau kunt doen. Het doornemen van de commentaar zou afdoende moeten zijn om een beeld te krijgen wat er allemaal precies gebeurt.

package test;

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

public class Test {

    public static void main(String[] args) throws IOException {

        // Geef de schijf op.
        File disk = new File("c:");
        
        // Kunnen we naar de schijf schrijven?
        if (!disk.canWrite()) {
            // Zo nee, stop het programma.
            throw new RuntimeException("Kan niet naar schijf schrijven.");
        }

        // Definieer de map.
        File dir = new File("c:/java");

        // Bestaat de map?
        if (!dir.exists()) {
            // Zo nee, aanmaken!
            dir.mkdir();
        }
        
        // Definieer het bestand.
        File file = new File("c:/java/test.txt");
        
        // Deze tekst moet in het bestand komen.
        String text = "Java forever!";

        // Bereid de writer voor.
        FileWriter writer = null;

        try {
            // Zet het bestand in de writer.
            writer = new FileWriter(file);
            
            // Schrijf de tekst naar het bestand. Let op: een
            // eventueel bestaand bestand wordt overgeschreven.
            writer.write(text);
        } catch (IOException e) {
            // Schrijven mislukt.
            System.err.println("Schrijven mislukt");

            // Werp de exceptie af, het programma moet direct stoppen.
            throw e;
        } finally {
            // Sluit de writer. Doe dit altijd in de finally blok!
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
        
        // Toon enkele gegevens.
        System.out.println("Is een bestand? " + file.isFile());
        System.out.println("Is een map? " + file.isDirectory());
        System.out.println("Is verborgen? " + file.isHidden());
        System.out.println("Is leesbaar? " + file.canRead());
        System.out.println("Is schrijfbaar? " + file.canWrite());
        System.out.println("Bestandsnaam: " + file.getName());
        System.out.println("Locatie: " + file.getParent());
        System.out.println("Pad: " + file.getPath());
        System.out.println("Grootte: " + file.length() + " bytes");

        // Hernoem het bestand.
        File newfile = new File("c:/java/newfile.txt");
        file.renameTo(newfile);

        // Toon bewijs van de hernoeming.
        System.out.println("Bestaat oud bestand? " + file.exists());
        System.out.println("Bestaat nieuw bestand? " + newfile.exists());
        System.out.println("Pad nieuw bestand? " + newfile.getPath());

        // Vraag de bestandsgrootte op als int.
        int length = (int) newfile.length();
        
        // Declareer en initialiseer een char array. De opgegeven grootte
        // moet in ieder geval minimaal de bestandsgrootte zijn.
        char[] content = new char[length];

        // Bereid de reader voor.
        FileReader reader = null;

        try {
            // Zet het bestand in de reader.
            reader = new FileReader(newfile);
            
            // Zet de inhoud in de char array.
            reader.read(content);
        } catch (IOException e) {
            // Lezen mislukt.
            System.err.println("Lezen mislukt");

            // Werp de exceptie af, het programma moet direct stoppen.
            throw e;
        } finally {
            // Sluit de reader. Doe dit altijd in de finally blok!
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }

        // Converteer van char array naar String en toon de inhoud.
        System.out.println("Inhoud: " + new String(content));

        // Verwijder het bestand.
        newfile.delete();
        
        // Verwijder de map. Dit gebeurt overigens alleen wanneer
        // de map helemaal leeg is. 
        dir.delete();
    }

}

Is een bestand? true
Is een map? false
Is verborgen? false
Is leesbaar? true
Is schrijfbaar? true
Bestandsnaam: test.txt
Locatie: c:\java
Pad: c:\java\test.txt
Grootte: 13 bytes
Bestaat oud bestand? false
Bestaat nieuw bestand? true
Pad nieuw bestand: c:\java\newfile.txt
Inhoud: Java forever!

Als je goed hebt opgelet, dan zie je dat de paden worden weergeven met een forward slash / in plaats van een backward slash \. Op zich werken ze allebei prima, echter de backward slash is binnen de String een escape teken; zo kun je bijvoorbeeld doublequotes binnen een string aangeven met "\"Quoted\"". Wanneer je per-se backward slashes in de paden wilt gebruiken, wat overigens af te raden is, zou je deze dus dubbel moeten opgeven:

        // Definieer het bestand.
        File file = new File("c:\\java\\test.txt");

Daarnaast werken alleen de paden met de forward slash ook prima in linux. Wanneer je in een Windows-omgeving geen schijfletter opgeeft, zoals "/java/test.txt", dan wordt er altijd uitgegaan van de partitie waar je de JVM hebt geinstalleerd en waarvandaan de JVM wordt uitgevoerd. Standaard is dit de c: schijf.

Wanneer je een pad en een bestandsnaam in twee variabelen hebt en je er niet helemaal zeker van bent of de slashes er wel goed in zitten, voordat je ze aan elkaar wilt plakken, dan zou de andere constructeur van File zeer van pas komen:

        // Definieer bij wijze van voorbeeld de pad en de naam.
        String filePath = "c:/java";
        String fileName = "test.txt";

        // Definieer het bestand.
        File file = new File(filePath, fileName);

Dat scheelt gehannes met indexOf() en/of matches() om te controleren of de slashes er wel goed in zitten.

Terug naar boven

Werken op directory niveau

Met de listFiles() methode van de File kun je alle directories en bestanden opvragen die in de aangegeven locatie zitten. Dit levert een File[] array op. Een simpel praktijkvoorbeeldje:

package test;

import java.io.File;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

public class Test {

    public static void main(String[] args) {

        // Geef de schijf op.
        File disk = new File("c:/");
        
        // Kunnen we van de schijf lezen?
        if (!disk.canRead()) {
            // Zo nee, stop het programma.
            throw new RuntimeException("Kan niet van schijf lezen.");
        }

        // Vraag de inhoud op.
        File[] files = disk.listFiles();

        // Voorbereid de collecties.
        Set diskDirs = new TreeSet();
        Set diskFiles = new TreeSet();
        
        // Scheid directories en bestanden. Ze worden namelijk
        // door elkaar in alfabetische volgorde opgevraagd.
        for (int i = 0; i < files.length; i++) {
            if (files[i].isDirectory()) {
                diskDirs.add(files[i]);
            } else if (files[i].isFile()) {
                diskFiles.add(files[i]);
            }
        }

        // Laat de directories zien.
        for (Iterator iter = diskDirs.iterator(); iter.hasNext();) {
            File diskDir = (File) iter.next();
            String name = diskDir.getName();
            System.out.println(name + " (DIR)");
        }

        // Laat de bestanden zien.
        for (Iterator iter = diskFiles.iterator(); iter.hasNext();) {
            File diskFile = (File) iter.next();
            String name = diskFile.getName();
            long size = diskFile.length();
            System.out.println(name + " (" + size + " bytes)");
        }
    }

}

Documents and Settings (DIR)
Program Files (DIR)
RECYCLER (DIR)
System Volume Information (DIR)
WINDOWS (DIR)
boot.ini (210 bytes)
Bootfont.bin (4952 bytes)
NTDETECT.COM (47564 bytes)
ntldr (251184 bytes)

Overigens kun je met de list() methode van de File direct de namen van alle directories en bestanden opvragen in de vorm van een String[] array.

Terug naar boven

FileReader en FileWriter

Met de FileReader en FileWriter kun je bestanden uitlezen en wegschrijven als een char array. Hier is een voorbeeld in de vorm van een eenvoudige bestands-kopie:

package test;

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

public class Test {

    public static void main(String[] args) {

        // Definieer het invoerbestand.
        File inputFile = new File("c:/java/input.txt");

        // Definieer het uitvoerbestand.
        File outputFile = new File("c:/java/output.txt");

        // Voorbereid de reader.
        FileReader reader = null;

        // Voorbereid de writer.
        FileWriter writer = null;

        try {
            // Laat het invoerbestand als een char array inlezen.
            reader = new FileReader(inputFile);

            // Laat het uitvoerbestand als een char array uitschrijven.
            writer = new FileWriter(outputFile);

            // Doorloop alle karakters ..
            int data = -1;
            while ((data = reader.read()) != -1) {
                // .. en kopieer elke karakter.
                writer.write(data);
            }
            
            // Klaar!
            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de reader en de writer. Doe dit altijd in de finally blok!
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}

De volgende manier kan trouwens ook, getuige het stukje code hierboven bij Werken op bestandsniveau:

package test;

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

public class Test {

    public static void main(String[] args) {

        File inputFile = new File("c:/java/input.txt");
        File outputFile = new File("c:/java/output.txt");
        FileReader reader = null;
        FileWriter writer = null;

        try {
            reader = new FileReader(inputFile);
            writer = new FileWriter(outputFile);

            // Downcast de hoeveelheid karakters van long naar int.
            int length = (int) inputFile.length();

            // Declareer en initialiseer een char array. De array
            // index accepteert alleen een int, vandaar de downcast.
            char[] content = new char[length];
            
            // Zet de inhoud van het invoerbestand in de char array.
            reader.read(content);

            // Zet de char array in het uitvoerbestand.
            writer.write(content);

            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

Deze manier is eigenlijk niet zo netjes, want je zou geheugenproblemen kunnen krijgen bij het initialiseren van de char array wanneer de length groter is dan de hoeveelheid beschikbare geheugen voor de JVM. Dit is echter wel véél sneller dan de andere methoden.

Daarnaast zou je ook problemen kunnen krijgen bij het downcasten van long naar int wanneer een bestand meer dan 2147483647 karakters bevat (de maximumwaarde van int). Aangezien 1 karakter gelijk staat aan 1 byte, mag de invoerbestand dus voor deze methode maximaal 2147483647 / 1024 / 1024 / 1024 = 2TB groot zijn (om precies te zijn: 2TB - 1 byte). Dat is in de meeste gevallen nog altijd een aardige hoeveelheid.

Indien je veel met grote datablokken werkt, dan kun je dus beter zélf een geheugenbuffer definiëren. In dit geval nemen we als voorbeeld 10KB.

package test;

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

public class Test {

    public static void main(String[] args) {

        File inputFile = new File("c:/java/input.txt");
        File outputFile = new File("c:/java/output.txt");
        FileReader reader = null;
        FileWriter writer = null;

        try {
            reader = new FileReader(inputFile);
            writer = new FileWriter(outputFile);

            // Voorbereid de hoeveelheid karakters.
            int length = 0;

            // Declareer en initialiseer een char array met een buffer van 10KB.
            char[] buffer = new char[10240];

            // Zet de inhoud van het invoerbestand in de char array buffer.
            // Er wordt elke keer 10KB van het invoerbestand in de buffer gezet.
            // Dit gaat zolang door tot er niks meer van het invoerbestand over is.
            while ((length = reader.read(buffer)) > 0) {
                // Zet de char array buffer in het uitvoerbestand.
                writer.write(buffer, 0, length);
            } 

            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

In plaats daarvan zou je ook gewoon BufferedReader en BufferedWriter kunnen gebruiken wanneer je graag grote bestanden wilt bufferen. Deze klassen regelen de buffering allemaal automatisch voor jou binnen de geheugenruimte van de JVM. Zie ook het volgende hoofdstuk hoe deze te gebruiken.

Overigens, als je verder niks met de bestanden hoeft te doen (gegevens opvragen, hernoemen, verwijderen, etc), dan kun je het bestand ook gewoon direct in de constructeur van FileReader en FileWriter definiëren in plaats van in de vorm van een File object:

            ...

            // Laat het invoerbestand als een char array inlezen.
            reader = new FileReader("c:/java/input.txt");

            // Laat het uitvoerbestand als een char array uitschrijven.
            writer = new FileWriter("c:/java/output.txt");

            ...
Terug naar boven

BufferedReader en BufferedWriter

Met BufferedReader en BufferedWriter kun je de inhoud van een willekeurige Reader respectievelijk Writer direct in een geheugenbuffer zetten en daarop te werken. Dit is sneller dan elke keer direct op het opslagmedium te werken (harde schijf, USB stick, CD/DVD, etc), met name bij grote bestanden en weinig geheugen.

package test;

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

public class Test {

    public static void main(String[] args) {

        // Voorbereid de gebufferde reader.
        BufferedReader reader = null;

        // Voorbereid de gebufferde writer.
        BufferedWriter writer = null;
        
        try {
            // Laat het invoerbestand als een gebufferde char array inlezen.
            reader = new BufferedReader(new FileReader("c:/java/input.txt"));

            // Laat het uitvoerbestand als een gebufferde char array uitschrijven.
            writer = new BufferedWriter(new FileWriter("c:/java/output.txt"));

            // Definieer een lege regel.
            String line;
            
            // Omdat het bestand nu in een geheugenbuffer zit kunnen we
            // volledige regels eruit halen. Dat gaan we nu ook doen:
            // Doorloop alle regels ..
            while ((line = reader.readLine()) != null) {
                // .. en kopieer elke regel.
                writer.write(line);
            }
            
            // Klaar!
            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de reader en de writer. Doe dit altijd in de finally blok!
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}
Terug naar boven

CharArrayReader en CharArrayWriter

Je kunt een voorgedefinieerde char array dus zó in de FileWriter#write() zetten:

package test;

import java.io.FileWriter;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        // Definieer de char array.
        char[] chars =
            new char[] {'J', 'a', 'v', 'a', ' ', 'F', 'o', 'r', 'e', 'v', 'e', 'r', '!'};

        // Zo kan het trouwens ook:
        // String string = "Java forever!";
        // char[] chars = string.toCharArray();
        // Je kunt trouwens ook een volledige String in de writer stoppen.

        // Voorbereid de writer.
        FileWriter writer = null;

        try {
            // Laat het uitvoerbestand als een char array uitschrijven.
            writer = new FileWriter("c:/java/output.txt");

            // Zet de char array in het uitvoerbestand.
            writer.write(chars);

            // Klaar!
            System.out.println("Uitschrijven gelukt.");
        } catch (IOException e) {
            System.err.println("Uitschrijven mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de writer. Doe dit altijd in de finally blok!
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}

Deze manier is eigenlijk niet zo netjes, want je zou geheugenproblemen kunnen krijgen bij het aanroepen van FileWriter#write() wanneer de char array groter is dan de hoeveelheid beschikbare geheugen voor de JVM. Dit is echter wel extreem snel wanneer je voldoende geheugen hebt.

Indien je veel met grote datablokken werkt, dan kun je dus beter een BufferedWriter gebruiken, maar dan moet je de voorgedefinieerde char array wel eerst in een automatisch gebufferde CharArrayReader zetten.

package test;

import java.io.BufferedWriter;
import java.io.CharArrayReader;
import java.io.FileWriter;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        // Definieer de char array.
        char[] chars =
            new char[] {'J', 'a', 'v', 'a', ' ', 'F', 'o', 'r', 'e', 'v', 'e', 'r', '!'};

        // Zo kan het trouwens ook:
        // String string = "Java forever!";
        // char[] chars = string.toCharArray();
        // Je kunt trouwens ook een volledige String in de writer stoppen.

        // Voorbereid de gebufferde writer.
        BufferedWriter writer = null;

        try {
            // Voorbereid de char array reader.
            CharArrayReader reader = new CharArrayReader(chars);

            // Laat het uitvoerbestand als een gebufferde char array uitschrijven.
            writer = new BufferedWriter(new FileWriter("c:/java/output.txt"));
            
            // Doorloop alle karakters ..
            int data = -1;
            while ((data = reader.read()) != -1) {
                // .. en schrijf elke karakter uit.
                writer.write(data);
            }

            // Klaar!
            System.out.println("Uitschrijven gelukt.");
        } catch (IOException e) {
            System.err.println("Uitschrijven mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de writer. Doe dit altijd in de finally blok!
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}

Hieronder volgt het omgekeerde voorbeeld met een CharArrayWriter:

package test;

import java.io.BufferedReader;
import java.io.CharArrayWriter;
import java.io.FileReader;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        // Voorbereid de gebufferde reader.
        BufferedReader reader = null;

        // Voorbereid de char array.
        char[] chars = null;

        try {
            // Laat het invoerbestand als een gebufferde char array inlezen.
            reader = new BufferedReader(new FileReader("c:/java/input.txt"));

            // Voorbereid de char array writer.
            CharArrayWriter writer = new CharArrayWriter();
            
            // Definieer een lege regel.
            String line;
            
            // Doorloop alle regels ..
            while ((line = reader.readLine()) != null) {
                // .. en kopieer elke regel.
                writer.write(line);
            }

            // Zet het resultaat in de byte array.
            chars = writer.toCharArray();

            // Klaar!
            System.out.println("Inlezen gelukt. Resultaat: " + new String(chars));
        } catch (IOException e) {
            System.err.println("Inlezen mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de reader. Doe dit altijd in de finally blok!
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}

Let op: de CharArrayReader en CharArrayWriter zijn dus automatisch gebufferd en het is derhalve volstrekt nutteloos om deze in een BufferedReader respectievelijk BufferedWriter te zetten.

Terug naar boven

FileInputStream en FileOutputStream

Met de FileInputStream en FileOutputStream kun je bestanden uitlezen en wegschrijven als een byte array. Hier is een voorbeeld in de vorm van een eenvoudige bestands-kopie:

package test;

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

public class Test {

    public static void main(String[] args) {

        // Definieer het invoerbestand.
        File inputFile = new File("c:/java/input.txt");

        // Definieer het uitvoerbestand.
        File outputFile = new File("c:/java/output.txt");

        // Voorbereid de inputstream.
        FileInputStream input = null;

        // Voorbereid de outputstream.
        FileOutputStream output = null;

        try {
            // Laat het invoerbestand als een byte array inlezen.
            input = new FileInputStream(inputFile);

            // Laat het uitvoerbestand als een byte array uitschrijven.
            output = new FileOutputStream(outputFile);

            // Doorloop alle bytes ..
            int data = -1;
            while ((data = input.read()) != -1) {
                // .. en kopieer elke byte.
                output.write(data);
            }

            // Klaar!
            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de input en de output. Doe dit altijd in de finally blok!
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}

De volgende manier kan trouwens ook:

package test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        FileInputStream input = null;
        FileOutputStream output = null;

        try {
            input = new FileInputStream("c:/java/input.txt");
            output = new FileOutputStream("c:/java/output.txt");

            // Declareer en initialiseer een byte array. De available()
            // methode van een InputStream retourneert een int, dus je
            // kunt deze direct gebruiken als de array index.
            byte[] content = new byte[input.available()];
            
            // Zet de inhoud van het invoerbestand in de byte array.
            input.read(content);

            // Zet de byte array in het uitvoerbestand.
            output.write(content);

            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

Deze manier is eigenlijk niet zo netjes, want je zou geheugenproblemen kunnen krijgen bij het initialiseren van de byte array wanneer de input.available() groter is dan de hoeveelheid beschikbare geheugen voor de JVM. Dit is echter wel véél sneller dan de andere methoden.

Indien je veel met grote datablokken werkt, dan kun je dus beter zélf een geheugenbuffer definiëren. In dit geval nemen we als voorbeeld 10KB.

package test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        FileInputStream input = null;
        FileOutputStream output = null;

        try {
            input = new FileInputStream("c:/java/input.txt");
            output = new FileOutputStream("c:/java/output.txt");

            // Voorbereid de hoeveelheid bytes.
            int length = 0;

            // Declareer en initialiseer een byte array met een buffer van 10KB.
            byte[] buffer = new byte[10240];

            // Zet de inhoud van het invoerbestand in de byte array buffer.
            // Er wordt elke keer 10KB van het invoerbestand in de buffer gezet.
            // Dit gaat zolang door tot er niks meer van het invoerbestand over is.
            while ((length = input.read(buffer)) > 0) {
                // Zet de byte array buffer in het uitvoerbestand.
                output.write(buffer, 0, length);
            } 

            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

In plaats daarvan zou je ook gewoon BufferedInputStream en BufferedOutputStream kunnen gebruiken wanneer je graag grote bestanden wilt bufferen. Deze klassen regelen de buffering allemaal automatisch voor jou binnen de geheugenruimte van de JVM. Zie ook het volgende hoofdstuk hoe deze te gebruiken.

Terug naar boven

BufferedInputStream en BufferedOutputStream

Met BufferedInputStream en BufferedOutputStream kun je de inhoud van een willekeurige InputStream respectievelijk OutputStream direct in een geheugenbuffer zetten en daarop te werken. Dit is sneller dan elke keer direct op het opslagmedium te werken (harde schijf, USB stick, CD/DVD, etc), met name bij grote bestanden en weinig geheugen.

package test;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        // Voorbereid de gebufferde inputstream.
        BufferedInputStream input = null;

        // Voorbereid de gebufferde outputstream.
        BufferedOutputStream output = null;

        try {
            // Laat het invoerbestand als een gebufferde byte array inlezen.
            input = new BufferedInputStream(new FileInputStream("c:/java/input.txt"));

            // Laat het uitvoerbestand als een gebufferde byte array uitschrijven.
            output = new BufferedOutputStream(new FileOutputStream("c:/java/output.txt"));
            
            // Doorloop alle bytes ..
            int data = -1;
            while ((data = input.read()) != -1) {
                // .. en kopieer elke byte.
                output.write(data);
            }

            // Klaar!
            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de input en de output. Doe dit altijd in de finally blok!
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}
Terug naar boven

ByteArrayInputStream en ByteArrayOutputStream

Je kunt een voorgedefinieerde byte array dus zó in de FileOutputStream#write() zetten:

package test;

import java.io.FileOutputStream;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        // Definieer de byte array.
        byte[] bytes = new byte[] {74, 97, 118, 97, 32, 102, 111, 114, 101, 118, 101, 114, 33};

        // Zo kan het trouwens ook:
        // String string = "Java forever!";
        // byte[] bytes = string.getBytes();

        // Voorbereid de outputstream.
        FileOutputStream output = null;

        try {
            // Laat het uitvoerbestand als een byte array uitschrijven.
            output = new FileOutputStream("c:/java/output.txt");

            // Zet de byte array in het uitvoerbestand.
            output.write(bytes);

            // Klaar!
            System.out.println("Uitschrijven gelukt.");
        } catch (IOException e) {
            System.err.println("Uitschrijven mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de output. Doe dit altijd in de finally blok!
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}

Deze manier is eigenlijk niet zo netjes, want je zou geheugenproblemen kunnen krijgen bij het aanroepen van FileOutputStream#write() wanneer de byte array groter is dan de hoeveelheid beschikbare geheugen voor de JVM. Dit is echter wel extreem snel wanneer je voldoende geheugen hebt.

Indien je veel met grote datablokken werkt, dan kun je dus beter een BufferedOutputStream gebruiken, maar dan moet je de voorgedefinieerde byte array wel eerst in een automatisch gebufferde ByteArrayInputStream zetten.

package test;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        // Definieer de byte array.
        byte[] bytes = new byte[] {74, 97, 118, 97, 32, 102, 111, 114, 101, 118, 101, 114, 33};

        // Zo kan het trouwens ook:
        // String string = "Java forever!";
        // byte[] bytes = string.getBytes();

        // Voorbereid de gebufferde outputstream.
        BufferedOutputStream output = null;

        try {
            // Voorbereid de byte array inputstream.
            ByteArrayInputStream input = new ByteArrayInputStream(bytes);

            // Laat het uitvoerbestand als een gebufferde byte array uitschrijven.
            output = new BufferedOutputStream(new FileOutputStream("c:/java/output.txt"));
            
            // Doorloop alle bytes ..
            int data = -1;
            while ((data = input.read()) != -1) {
                // .. en kopieer elke byte.
                output.write(data);
            }

            // Klaar!
            System.out.println("Uitschrijven gelukt.");
        } catch (IOException e) {
            System.err.println("Uitschrijven mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de output. Doe dit altijd in de finally blok!
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}

Hieronder volgt het omgekeerde voorbeeld met een ByteArrayOutputStream:

package test;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {

        // Voorbereid de gebufferde inputstream.
        BufferedInputStream input = null;

        // Voorbereid de byte array.
        byte[] bytes = null;

        try {
            // Laat het invoerbestand als een gebufferde byte array inlezen.
            input = new BufferedInputStream(new FileInputStream("c:/java/input.txt"));

            // Vraag de hoeveelheid bytes op.
            int length = input.available();

            // Voorbereid de byte array outputstream.
            ByteArrayOutputStream output = new ByteArrayOutputStream(length);

            // Doorloop alle bytes ..
            int data = -1;
            while ((data = input.read()) != -1) {
                // .. en kopieer elke byte.
                output.write(data);
            }

            // Zet het resultaat in de byte array.
            bytes = output.toByteArray();

            // Klaar!
            System.out.println("Inlezen gelukt. Resultaat: " + new String(bytes));
        } catch (IOException e) {
            System.err.println("Inlezen mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de input. Doe dit altijd in de finally blok!
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}

Let op: de ByteArrayInputStream en ByteArrayOutputStream zijn dus automatisch gebufferd en het is derhalve volstrekt nutteloos om deze in een BufferedInputStream respectievelijk BufferedOutputStream te zetten.

Terug naar boven

Converteren van InputStream naar Reader

Met InputStreamReader kun je de inhoud van een willekeurige byte array InputStream converteren naar een char array Reader. Dit is handig wanneer je platte tekstbestanden in vorm van een byte array krijgt aangeleverd (uit een ander programma, uit het geheugen, uit een file-upload element op een Java EE webapplicatie, etcetera).

package test;

import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;

public class Test {

    public static void main(String[] args) {

        // Voorbereid de reader.
        InputStreamReader reader = null;

        // Voorbereid de writer.
        FileWriter writer = null;

        try {
            // Voorbereid het invoerbestand als een byte array.
            FileInputStream input = new FileInputStream("c:/java/input.txt");

            // Laat het invoerbestand als een char array inlezen.
            reader = new InputStreamReader(input);

            // Laat het uitvoerbestand als een char array uitschrijven.
            writer = new FileWriter("c:/java/output.txt");

            // Doorloop alle karakters ..
            int data = -1;
            while ((data = reader.read()) != -1) {
                // .. en kopieer elke karakter.
                writer.write(data);
            }

            // Klaar!
            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de reader en de writer. Doe dit altijd in de finally blok!
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}
Terug naar boven

Converteren van Writer naar OutputStream

Met OutputStreamWriter kun je een byte array OutputStream voorbereiden voor de invoer van een char array. Het doet zich dus voor als een Writer, maar het schrijft weg in vorm van een byte array. Dit is bijvoorbeeld handig wanneer je zelf geschreven binaire bestanden wilt wegschrijven.

package test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class Test {

    public static void main(String[] args) {

        // Definieer het bestand.
        File inputFile = new File("c:/java/input.txt");

        // Voorbereid de reader.
        FileReader reader = null;

        // Voorbereid de writer.
        OutputStreamWriter writer = null;

        try {
            // Laat het invoerbestand als een char array inlezen.
            reader = new FileReader(inputFile);

            // Voorbereid het uitvoerbestand als een byte array.
            FileOutputStream output = new FileOutputStream("c:/java/output.txt");

            // Laat het uitvoerbestand als een char array uitschrijven.
            writer = new OutputStreamWriter(output);

            // Doorloop alle karakters ..
            int data = -1;
            while ((data = reader.read()) != -1) {
                // .. en kopieer elke karakter.
                writer.write(data);
            }

            // Klaar!
            System.out.println("Kopieren gelukt.");
        } catch (IOException e) {
            System.err.println("Kopieren mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de reader en de writer. Doe dit altijd in de finally blok!
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    // Niks aan te doen.
                    e.printStackTrace();
                }
            }
        }
    }

}
Terug naar boven

De bestanden worden overgeschreven?

Inderdaad, het overschrijven van eventueel bestaande bestanden is de standaard gedrag van de Writer en de OutputStream. Maar deze interfaces kennen een tweede constructeur, waarbij je met een boolean true kunt aangeven dat het te schrijven bestand niet overgeschreven mag worden. De geleverde data wordt dan gewoon achteraan toegevoegd.

Dit kun je dus als volgt aangeven:

        // Writer: geef aan dat een bestaand bestand behouden moet worden.
        FileWriter writer = new FileWriter("c:/java/output.txt", true);

        // OutputStream: geef aan dat een bestaand bestand behouden moet worden.
        FileOutputStream output = new FileOutputStream("c:/java/output.txt", true);
Terug naar boven

Copyright - Er is geen copyright op de code. Je kunt het naar believen overnemen, aanpassen danwel verspreiden.

(C) Juni 2006, BalusC