Showing posts with label Dutch. Show all posts
Showing posts with label Dutch. Show all posts

Friday, March 2, 2007

JDBC tutorial (Dutch)

Introductie

Java kent een aantal standaard klassen waarmee je met SQL databases kunt communiceren, deze klassen (eigenlijk interfaces) zitten in het java.sql package onder de noemer "JDBC API" (Java DataBase Connectivity Application Programming Interface). Hieronder staan enkele klassen en methoden die van belang zijn bij de communicatie met SQL databases.

java.sql klasse beschrijving
DriverManager Beheert de JDBC drivers.
Connection Opent een sessie met een specifieke database.
Statement Een open statement om SQL queries mee te versturen en resultaten te ontvangen. De volgende twee methoden zijn van nut:
executeQuery(sql): nuttig voor SELECT queries en retourneert een ResultSet.
executeUpdate(sql): nuttig voor INSERT, UPDATE en DELETE queries en retourneert de hoeveelheid getroffen rijen in vorm van een int.
PreparedStatement Een open statement met een voorbereide SQL query waaraan je slechts de waarden hoeft toe te voegen. Zeer nuttig om eventuele SQL injecties te voorkomen doordat je met specifieke objecten kunt werken in plaats van met strings. Bij de stringconversies worden de aanhalingstekens automatisch opgevangen. De volgende twee methoden zijn van nut:
executeQuery(): nuttig voor SELECT queries en retourneert een ResultSet.
executeUpdate(): nuttig voor INSERT, UPDATE en DELETE queries en retourneert de hoeveelheid getroffen rijen in vorm van een int.
ResultSet Het resultaat van Statement#executeQuery() zal in een ResultSet gezet worden.

Het enige extra wat je buiten de standaard JDBC API nodig hebt is een geldige JDBC driver voor de gewenste database. De JDBC driver is grof gezegd de concrete implementatie van de JDBC API, dat bijna louter uit interfaces bestaat. Deze kun je normaliter als een JAR bestand downloaden, die je dan in de classpath moet zetten. Voorbeelden: MySQL Connector/J, DB2 Driver for JDBC and SQLJ, Oracle JDBC Driver, PostgreSQL JDBC Driver, Microsoft SQL Server JDBC Driver, etcetera.

Terug naar boven

Foutenafhandeling

Praktisch alle klassen van de java.sql package kunnen een SQLException of een subklasse daarvan afwerpen. Je bent dus verplicht om de SQL acties in een try-catch-finally statement te zetten. Anders moet je desbetreffende SQLException doorgooien via de throws bepaling van de methode.

Het is zeer aanbevelenswaardig om de database sessie (Connection) na het gebruik altijd te sluiten met de close() methode om de systeembronnen te vrijgeven. Deze wordt namelijk niet automatisch direct na gebruik gesloten. Als je dat niet doet, dan kun je wanneer je veel connecties maakt na een tijdje een tekort aan systeembronnen krijgen, met alle desastreuze gevolgen van dien.

Wanneer je de Connection sluit, dan zullen de 'goede' JDBC drivers ook alle binnen dezelfde sessie geopende statementen en resultsets ook automatisch gesloten worden. Wanneer je een afzonderlijke Statement of PreparedStatement binnen dezelfde sessie sluit, dan zullen de 'goede' JDBC drivers alle geopende resultsets ook automatisch sluiten, maar de connectie blijft nog wel open. Wanneer je een afzonderlijke ResultSet binnen dezelfde sessie sluit, dan zul je er niet meer doorheen kunnen lopen, maar blijven de connectie en de statementen nog wel open. Wanneer je binnen een connectie meerdere statementen en/of resultsets wilt gaan openen, dan is het verstandig om deze direct te sluiten na het het verwerken ervan, want de hoeveelheid tegelijkertijd geopende statementen en resultsets is niet ongelimiteerd. Wanneer je binnen een statement meerdere resultsets wilt gaan openen, dan zullen de 'goede' JDBC drivers de eerder geopende resultsets automatisch sluiten.

Ondanks dat 'goede' JDBC drivers voor het automatisch sluiten zorgen, is het jouw taak als een 'goede' developer om alle connecties, statementen en resultsets zelf te sluiten! Anders breekt jouw hele applicatie wanneer iemand een 'slechte' JDBC driver gebruikt.

Terug naar boven

JDBC driver installeren

Hier zullen we van een MySQL database uitgaan. MySQL is de meest gebruikte freeware database. Hoe MySQL te installeren valt buiten de scope van dit artikel. Het wijst praktisch van zichzelf uit: even MySQL downloaden en dan de installer uitvoeren.

Om een SQL database vanuit Java te kunnen benaderen zul je dus de bijbehorende JDBC driver moeten downloaden en installeren, voor MySQL is dit de MySQL Connector/J. Download bij voorkeur de meest recente versie, op het moment van schrijven is dit versie 5.0. Download hier het zip bestand. Pak deze zip uit en haal daar het JAR bestand uit, in dit geval heet deze mysql-connector-java-5.0.8-bin.jar (let op: de versie nummer kan verschillen).

Update: er is een nieuwere Connector/J beschikbaar, de versie 5.1. Hiervoor heb je echter minimaal Java 6.0 nodig, aangezien de JDBC 4.0 specificatie pas in Java 6.0 is geintroduceerd. De Connector/J 5.0 werkt nog wel op Java 5.0 met de JDBC 3.0 specificatie.

Om deze driver in je Java code te kunnen gebruiken, zul je deze eerst in de classpath moeten zetten. Wanneer je het lokaal buiten de IDE wil gebruiken, dan zul je mogelijk eerst de omgevingsvariabele classpath moeten definieren waar je de JAR's kunt neerplanten, dit staat hier uitgelegd: Uitvoeren - Classpath. Wanneer je het in een applicatieserver wil gebruiken, dan zul je deze JAR's in de classpath van de applicatieserver moeten zetten, gewoonlijk is dit de /lib directory. Binnen een IDE, zoals Eclipse, volstaat het om deze driver aan de Java Build Path toe te voegen, zie de onderstaande procedure:

  1. Maak eventueel een project aan: File - New - Project... - Java Project, klik op Next, geef het een naam, bijvooorbeeld "MyDao", laat de rest van de velden standaard en klik op Finish.
  2. Importeer de JAR; rechtsklik op dit project: Import... - General - File System, klik op Next, wijs de directory aan waar het mysql-connector-java-5.0.8-bin.jar bestand zit, vink het bestand aan en klik op Finish.
    Import MySQL JAR
  3. Voeg de JAR tenslotte toe aan de classpath van het project; rechtsklik op het project - Properties - Java Build Path - Libraries - Add JARs - selecteer de zojuist geimporteerde JAR en klik op Finish.
    MySQL JAR in Build PathMySQL JAR in Project

Voor alle andere databases geldt hetzelfde procedure: download de JAR(s) en zet het in de classpath. Bij sommige database servers worden inderdaad meerdere JAR's geleverd, zoals bij DB2.

Terug naar boven

Database voorbereiden

Ter voorbereiding: we gaan hier ervan uit dat je de SQL basics onder de knie hebt. JDBC en SQL staan op zich volledig los van elkaar: de SQL queries moet je zelf schrijven, dat doet JDBC niet voor jou. Het verzorgt slechts de communicatie tussen de Java code en de SQL database. Mocht SQL jou ook niet helemaal bekend zijn, dan kan het waard zijn jezelf daar eerst in te verdiepen: SQL tutorials op het Internet.

We zullen nu eerst even een voorbeeld MySQL database tabel met een auto-generated technische ID veld "ID", een alfanummeriek veld "Name" en een nummeriek veld "Value" voorbereiden. Voer de onderstaande MySQL SQL uit in de database:

CREATE DATABASE javabase;
CREATE TABLE javabase.maintable (
    ID BIGINT AUTO_INCREMENT PRIMARY KEY,
    Name VARCHAR(255),
    Value INT
);

Let op: de SQL CREATE commando's kunnen per database verschillen. Voor bijvoorbeeld DB2, Oracle, PostgreSQL en Microsoft SQL Server moet je zo'n tabel op een iets andere manier aanmaken, omdat ze met databaseschema's werken, iets wat MySQL nog volledig onbekend is. Daarnaast heeft iedere database een andere implementatie van een auto-generated ID veld. Hier staat een rijke bron aan database-specifieke SQL commando's: SQLzoo.net.

Hieronder staat hoe je een vergelijkbaar tabel in DB2 kunt aanmaken:

CREATE DATABASE javabase;
CREATE SCHEMA schema;
CREATE TABLE javabase.schema.maintable (
    ID BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    Name VARCHAR(255),
    Value INT
);

En de Oracle en PostgreSQL equivalent:

CREATE DATABASE javabase;
CREATE SCHEMA schema;
CREATE TABLE javabase.schema.maintable (
    ID SERIAL PRIMARY KEY,
    Name VARCHAR(255),
    Value INT
);

Noot: deze query maakt ook automatisch een sequence aan in javabase.schema.maintable_id_seq.

En tenslotte die voor de Microsoft SQL Server:

CREATE DATABASE javabase;
CREATE SCHEMA schema;
CREATE TABLE javabase.schema.maintable (
    ID BIGINT IDENTITY PRIMARY KEY,
    Name VARCHAR(255),
    Value INT
);
Terug naar boven

JDBC URL voorbereiden

Om een database tabel vanuit de Java code te kunnen aanroepen heb je een JDBC URL nodig. Deze is in het geval van MySQL databases als volgt opgebouwd:

jdbc:mysql://hostname:port/database?user=username&password=password

hostname: verplicht, de host naam of de IP. Bijvoorbeeld: localhost of 127.0.0.1
port: optioneel, de poort van de database. Bijvoorbeeld: 3306
database: verplicht, de naam van de database. Bijvoorbeeld: javabase
username: optioneel, de inlog naam voor de database. Bijvoorbeeld: root
password: optioneel, de wachtwoord voor de database. Bijvoorbeeld: d$7hF_r!9Y

Uitgaande van een MySQL database op je eigen computer (localhost) achter de standaard poort 3306 (die je eigenlijk gewoon kunt weglaten) en een gebruikeraccount root met de wachtwoord d$7hF_r!9Y, zou onze MYSQL JDBC URL er zo uitzien:

  jdbc:mysql://localhost:3306/javabase?user=root&password=d$7hF_r!9Y

Let op: dit verschilt dus per database type, zie ook de documentatie bij de JDBC driver. De onderstaande voorbeelden zijn uit desbetreffende documentatie gehaald. Wanneer deze URL's om een of andere reden niet lekker werken, dan zul je het beste even zelf de JDBC documentatie moeten doornemen.

Voor een DB2 server die standaard op poort 50000 zit ziet een vergelijkbare JDBC URL als volgt eruit:

  jdbc:db2://localhost:50000/javabase:user=root;password=d$7hF_r!9Y

Voor Oracle die standaard achter poort 1521 bereikbaar is moet je de JDBC URL als volgt formuleren:

  jdbc:oracle:thin:root/d$7hF_r!9Y@//localhost:1521/javabase

Voor PostgreSQL gelden dezelfde richtlijnen als MySQL, behalve dan dat het als standaard poort 5432 gebruikt:

  jdbc:postgresql://localhost:5432/javabase?user=root&password=d$7hF_r!9Y

Voor de Microsoft SQL Server die standaard op poort 1433 zit zou zo'n JDBC URL als volgt eruitzien:

  jdbc:sqlserver://localhost:1344;databaseName=javabase;user=root;password=d$7hF_r!9Y

Terug naar boven

Eenvoudige INSERT

Hieronder volgt een compleet werkend voorbeeld van een eenvoudige INSERT actie op de database.

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {

    public static void main(String[] args) {

        // Laad eerst de driver.
        try {
            Class.forName("com.mysql.jdbc.Driver");

            // Voor DB2 laad je de driver als volgt:
            // Class.forName("com.ibm.db2.jcc.DB2Driver");

            // Voor Oracle laad je de driver als volgt:
            // Class.forName("oracle.jdbc.driver.OracleDriver");

            // Voor PostgreSQL laad je de driver als volgt:
            // Class.forName("org.postgresql.Driver");

            // Voor Microsoft SQL Server laad je de driver als volgt:
            // Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");

            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        // Voorbereid de connectie, statement, resultset, URL en INSERT query.
        Connection connection = null;
        Statement statement = null;
        ResultSet generatedKeys = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String insertQuery = "INSERT INTO maintable (name, value) VALUES ('testnaam', 10)";

        // Voor DB2, Oracle, PostgreSQL en MSSQL moet je ook de database schema aanwijzen:
        // INSERT INTO schema.maintable (name, value) VALUES ('testnaam', 10)

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url);

            // Verkrijg de statement.
            statement = connection.createStatement();

            // Voer de INSERT query uit.
            int affectedRows = statement.executeUpdate(insertQuery);

            // Verkrijg de INSERT ID.
            if (affectedRows == 1) {
                generatedKeys = statement.getGeneratedKeys();

                // Let op: het is afhankelijk van de JDBC driver of dit werkt! Bijvoorbeeld de
                // Oracle en PostgreSQL JDBC drivers ondersteunen dit niet. Je zult deze als
                // sequence moeten opvragen, waarbij de sequence naam in regel als volgt is:
                // "schemanaam.tabelnaam" + "_id_seq". Doe dit wel binnen dezelfde statement!
                // 
                // String sequenceQuery = "SELECT currval('schema.maintable_id_seq')";
                // generatedKeys = statement.executeQuery(sequenceQuery);

                if (generatedKeys.next()) {
                    long insertID = generatedKeys.getLong(1);
                    System.out.println("Insert ID is: " + insertID);
                }
            }

            // Klaar!
            System.out.println("Uitvoeren van de INSERT query is gelukt.");
        } catch (SQLException e) {
            // Foutje?
            System.err.println("Uitvoeren van de INSERT query is mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de resultset, statement en connectie. Doe dit altijd in de finally blok!
            if (generatedKeys != null) {
                try {
                    generatedKeys.close();
                } catch (SQLException e) {
                    // Niks aan te doen.
                    System.err.println("Kan de resultset niet sluiten.");
                    e.printStackTrace();
                }
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    // Niks aan te doen.
                    System.err.println("Kan de statement niet sluiten.");
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    // Niks aan te doen.
                    System.err.println("Kan de connectie niet sluiten.");
                    e.printStackTrace();
                }
            }
        }
    }

}

Laden van de driver is gelukt.
Insert ID is: 1
Uitvoeren van de INSERT query is gelukt.

Je kunt de connectie ook op een andere manier verkrijgen, de DriverManager.getConnection() is ook op de volgende manier te gebruiken:

        ...

        // Voorbereid de URL, de gebruikersnaam en de wachtwoord.
        String url = "jdbc:mysql://localhost/javabase";
        String username = "root";
        String password = "d$7hF_r!9Y";

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url, username, password);

            ...

Je kunt dus de username en de password gedeelten uit de JDBC URL weghalen en deze apart doorgeven.

Omdat het sluiten van de connectie, statement en resultset vaker dan eens zal gebeuren, is het handiger om deze te refactoren naar een utility klasse met static methoden dat meer dan eens aangeroepen kan worden. Hieronder staat een voorbeeld:

package test;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public final class SqlUtil {

    public static void close(Connection connection) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                // Niks aan te doen.
                System.err.println("Kan de connectie niet sluiten.");
                e.printStackTrace();
            }
        }
    }

    public static void close(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                // Niks aan te doen.
                System.err.println("Kan de statement niet sluiten.");
                e.printStackTrace();
            }
        }
    }

    public static void close(ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                // Niks aan te doen.
                System.err.println("Kan de resultset niet sluiten.");
                e.printStackTrace();
            }
        }
    }

}

Die kun je dan als volgt in de finally blok gebruiken:

            ...

        } finally {
            // Sluit de resultset, statement en connectie. Doe dit altijd in de finally blok!
            SqlUtil.close(generatedKeys);
            SqlUtil.close(statement);
            SqlUtil.close(connection);
        }

Terug naar boven

Eenvoudige SELECT

Hieronder volgt een compleet werkend voorbeeld van een eenvoudige SELECT actie op de database.

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {

    public static void main(String[] args) {

        // Laad eerst de driver.
        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        // Voorbereid de connectie, statement, resultset, URL en SELECT query.
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String selectQuery = "SELECT id, name, value FROM maintable WHERE name = 'testnaam'";

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url);

            // Verkrijg de statement.
            statement = connection.createStatement();

            // Voer de SELECT query uit.
            resultSet = statement.executeQuery(selectQuery);

            // Verwerk de resultaten.
            while (resultSet.next()) {
                long id = resultSet.getLong("id");
                String name = resultSet.getString("name");
                int value = resultSet.getInt("value");
                System.out.println("ID=" + id + ",Name=" + name + ",Value=" + value);
            }

            // Klaar!
            System.out.println("Uitvoeren van de SELECT query is gelukt.");
        } catch (SQLException e) {
            // Foutje?
            System.err.println("Uitvoeren van de SELECT query is mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de resultset, statement en connectie. Doe dit altijd in de finally blok!
            SqlUtil.close(resultSet);
            SqlUtil.close(statement);
            SqlUtil.close(connection);
        }
    }

}

Laden van de driver is gelukt.
ID=1,Name=testnaam,Value=10
ID=2,Name=testnaam,Value=10
ID=3,Name=testnaam,Value=10
Uitvoeren van de SELECT query is gelukt.

De eerder beschreven INSERT query werd inderdaad eerst driemaal uitgevoerd ;) De ResultSet van de SELECT query kun je trouwens ook op de volgende manier verwerken, met kolomnummers in plaats van kolomnamen. Hierbij kun je gewoon de volgorde van de kolommen van de database aanhouden en het begint altijd met 1 (en dus niet met 0!).

            // Verwerk de resultaten.
            while (resultSet.next()) {
                long id = resultSet.getLong(1);
                String name = resultSet.getString(2);
                int value = resultSet.getInt(3);
                System.out.println("ID=" + id + ",Name=" + name + ",Value=" + value);
            }

Deze methode is fractioneel sneller dan het gebruik van volledige kolomnamen, het scheelt in geval van de betere JDBC drivers ongeveer een halve procent tot één procent van de tijd.

Als je er niet helemaal zeker van bent in welk soort datatype of object je de verkregen waarde moet stoppen en/of wanneer je ClassCastException foutmeldingen krijgt, dan kun je ook proefondervindelijk getObject() van de resultset gebruiken om het resultaat te upcasten naar een Object. Met Object#getClass() kun je tenslotte de geinstantieerde klasse opvragen en derhalve de 'automatisch' door de driver vertaalde klasse achterhalen:

            // Achterhaal het vertaalde object type van de ID veld.
            if (resultSet.next()) {
                Object id = resultSet.getObject("ID");
                System.out.println(id.getClass());
            }

class java.lang.Long

In dit geval wordt een MySQL BIGINT veld dus door de JDBC driver geconverteerd naar een Long. Je zou de waarde dus het beste in het primitieve datatype long of in de wrapper datatype object Long moeten stoppen.

Terug naar boven

Eenvoudige UPDATE

Hieronder volgt een compleet werkend voorbeeld van een eenvoudige UPDATE actie op de database.

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {

    public static void main(String[] args) {

        // Laad eerst de driver.
        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        // Voorbereid de connectie, statement, URL en UPDATE query.
        Connection connection = null;
        Statement statement = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String updateQuery = "UPDATE maintable SET name = 'anderenaam' WHERE id = 1";

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url);

            // Verkrijg de statement.
            statement = connection.createStatement();

            // Voer de UPDATE query uit.
            int affectedRows = statement.executeUpdate(updateQuery);
            System.out.println("Aantal getroffen rijen: " + affectedRows);

            // Klaar!
            System.out.println("Uitvoeren van de UPDATE query is gelukt.");
        } catch (SQLException e) {
            // Foutje?
            System.err.println("Uitvoeren van de UPDATE query is mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de statement en connectie. Doe dit altijd in de finally blok!
            SqlUtil.close(statement);
            SqlUtil.close(connection);
        }
    }

}

Laden van de driver is gelukt.
Aantal getroffen rijen: 1
Uitvoeren van de UPDATE query is gelukt.

Terug naar boven

Eenvoudige DELETE

Hieronder volgt een compleet werkend voorbeeld van een eenvoudige DELETE actie op de database.

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {

    public static void main(String[] args) {

        // Laad eerst de driver.
        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        // Voorbereid de connectie, statement, URL en DELETE query.
        Connection connection = null;
        Statement statement = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String deleteQuery = "DELETE FROM maintable WHERE name = 'anderenaam'";

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url);

            // Verkrijg de statement.
            statement = connection.createStatement();

            // Voer de DELETE query uit.
            int affectedRows = statement.executeUpdate(deleteQuery);
            System.out.println("Aantal getroffen rijen: " + affectedRows);

            // Klaar!
            System.out.println("Uitvoeren van de DELETE query is gelukt.");
        } catch (SQLException e) {
            // Foutje?
            System.err.println("Uitvoeren van de DELETE query is mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de statement en connectie. Doe dit altijd in de finally blok!
            SqlUtil.close(statement);
            SqlUtil.close(connection);
        }
    }

}

Laden van de driver is gelukt.
Aantal getroffen rijen: 1
Uitvoeren van de DELETE query is gelukt.

Terug naar boven

Voorbereide statementen

Met PreparedStatement kun je een statement voorbereiden, waarbij je de openstaande waarden vult met een vraagteken "?". Dit is niet alleen handig voor veelgebruikte statementen, maar ook om SQL injecties te voorkomen, bij de stringconversies binnen de PreparedStatement worden de aanhalingstekens namelijk automatisch opgevangen. Hieronder volgt een voorbeeld van een voorbereide INSERT statement. De waarden moet je in de volgorde toevoegen zoals de vraagtekens in de voorbereide statement staan en de index begint altijd met 1 (en dus niet met 0!).

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Test {

    public static void main(String[] args) {

        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet generatedKeys = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";

        // Voorbereid de INSERT query.
        String preparedInsertQuery = "INSERT INTO maintable (name, value) VALUES (?, ?)";

        try {
            connection = DriverManager.getConnection(url);

            // Verkrijg de voorbereide statement.
            preparedStatement = connection.prepareStatement(preparedInsertQuery);

            // Voeg de waarden toe.
            preparedStatement.setString(1, "testname");
            preparedStatement.setInt(2, 10);

            // Voer de INSERT query uit.
            preparedStatement.executeUpdate();

            generatedKeys = preparedStatement.getGeneratedKeys();
            if (generatedKeys.next()) {
                long insertID = generatedKeys.getLong(1);
                System.out.println("Insert ID is: " + insertID);
            }

            System.out.println("Uitvoeren van de INSERT query is gelukt.");
        } catch (SQLException e) {
            System.err.println("Uitvoeren van de INSERT query is mislukt.");
            e.printStackTrace();
        } finally {
            SqlUtil.close(generatedKeys);
            SqlUtil.close(preparedStatement);
            SqlUtil.close(connection);
        }
    }

}

Laden van de driver is gelukt.
Insert ID is: 4
Uitvoeren van de INSERT query is gelukt.

Noot: de SqlUtil hoeft niet uitgebreid te worden met een nieuwe close() methode voor de PreparedStatement. Aangezien deze een subklasse is van Statement, wordt de close() methode daarvan gewoon gebruikt.

Terug naar boven

DTO's zijn een must

Het is een zeer goede practice om het verkregen ResultSet te omvertalen naar een lijst met DTO's (Data Transfer Objects). Een DTO moet in dit geval een volledige rij van een database tabel voorstellen. De DTO's zouden de Javabean specificatie moeten volgen: de velden (properties) worden private gemaakt en deze zijn alleen toegankelijk via public getters en setters (de accessors). Dit is overigens het schoolvoorbeeld van encapsulation. Hieronder volgt een voorbeeld van een DTO afspiegeling van de "maintable" tabel:

package test;

public class Maintable {

    // Properties -------------------------------------------------------------------------------

    private Long id;
    private String name;
    private Integer value;

    // Getters ----------------------------------------------------------------------------------

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Integer getValue() {
        return value;
    }

    // Setters ----------------------------------------------------------------------------------

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setValue(Integer value) {
        this.value = value;
    }

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

    // Dit is niet verplicht, maar gewoon handig. Het overschrijft de
    // Object#toString() zodat je een mooie String representatie krijgt.
    public String toString() {
        return "[ID=" + id + ",Name=" + name + ",Value=" + value + "]";
    }

}

Het kan handig zijn om wrapper datatype objecten (Long, Integer, Boolean, etc) in plaats van primitieve datatypen (long, int, boolean, etc) voor de properties te gebruiken, met name omdat deze wrapper datatype objecten in tegenstelling tot primitieve datatypen ook null waarden kunnen bevatten, waarmee je zou kunnen aangeven dat het veld nooit is ingevuld. Daarnaast kunnen de "primitieve velden" van de database ook NULL waarden bevatten danwel accepteren die je op geen enkele manier naar een primitieve datatype kunt vertalen. Pas wanneer een database veld strikt als NOT NULL is gespecificeerd, dan kun je daarvoor wel gerust een primitieve datatype gebruiken. Zie verder ook Java Tutorial - Datatypen en Data conversies.

Deze DTO kun je in het voorbeeld van een voorbereide SELECT als volgt toepassen:

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String preparedSelectQuery = "SELECT id, name, value FROM maintable WHERE name = ?";

        // Voorbereid de DTO lijst. Dit mag trouwens ook een Set zijn, wat je maar wil.
        // Een List is makkelijker sorteerbaar en verwerkbaar.
        List<Maintable> results = new ArrayList<Maintable>();

        try {
            connection = DriverManager.getConnection(url);
            preparedStatement = connection.prepareStatement(preparedSelectQuery);
            preparedStatement.setString(1, "testnaam");
            resultSet = preparedStatement.executeQuery();

            // Verwerk de resultaten in een DTO lijst.
            while (resultSet.next()) {
                Maintable maintable = new Maintable();
                maintable.setId(new Long(resultSet.getLong("id")));
                maintable.setName(resultSet.getString("name"));
                maintable.setValue(new Integer(resultSet.getInt("value")));
                results.add(maintable);
            }

            System.out.println("Uitvoeren van de SELECT query is gelukt.");
            System.out.println("Aantal gevonden resultaten: " + results.size());
        } catch (SQLException e) {
            System.err.println("Uitvoeren van de SELECT query is mislukt.");
            e.printStackTrace();
        } finally {
            SqlUtil.close(resultSet);
            SqlUtil.close(preparedStatement);
            SqlUtil.close(connection);
        }

        // Doorloop de DTO lijst.
        for (Maintable result : results) {
            System.out.println(result);
        }
    }

}

Laden van de driver is gelukt.
Uitvoeren van de SELECT query is gelukt.
Aantal gevonden resultaten: 3
[ID=2,Name=testnaam,Value=10]
[ID=3,Name=testnaam,Value=10]
[ID=4,Name=testnaam,Value=10]

Zo'n lijst met DTO's kun je dan verder gebruiken buiten de database-communicatie-laag van je applicatie.

Terug naar boven

Een universele DAO ontwerpen?

De lappen code hierboven zijn in principe erg basaal en veel ervan is hetzelfde. Het is de kunst om deze lappen code netjes te "refactoren" in aparte klassen en methoden, zodat geen enkel stukje code dubbel voorkomt. Deze klassen zouden dan tezamen dan een Data Access Layer vormen met een generieke DAO (Data Access Object) dat alle queries zou moeten kunnen afhandelen. We gaan hier in dit artikel niet verder op in, maar om een idee te geven staat hieronder een voorbeeld van een SELECT query met behulp van een uitgekiende DAO:

package test;

import net.balusc.dao.DaoException;
import net.balusc.dao.DaoSession;
import net.balusc.dao.DatabaseType;
import net.balusc.dto.DtoList;
import net.balusc.query.LoadQuery;
import net.balusc.testdata.Maintable;

public class Test {

    private static DaoSession daoSession = new DaoSession(DatabaseType.MYSQL);

    static {
        daoSession.setUrl("jdbc:mysql://localhost/javabase");
        daoSession.setUsername("root");
        daoSession.setPassword("d$7hF_r!9Y");
    }

    public static void main(String[] args) {

        Maintable example = new Maintable();
        example.setName("testnaam");
        LoadQuery<Maintable> loadQuery = new LoadQuery<Maintable>(example);
        
        try {
            daoSession.execute(loadQuery);
        } catch (DaoException e) {
            e.printStackTrace();
            return;
        }

        DtoList<Maintable> results = loadQuery.getOutput();

        for (Maintable result : results) {
            System.out.println(result);
        }
    }

}

[INFO] net.balusc.sql.PreparedQuery#executeSelectQuery: SELECT Maintable.ID AS Maintable_ID, Maintable.Name AS Maintable_Name, Maintable.Value AS Maintable_Value FROM Maintable WHERE Maintable.Name = ? [testnaam]
[ID=2,Name=testnaam,Value=10]
[ID=3,Name=testnaam,Value=10]
[ID=4,Name=testnaam,Value=10]

Een wat 'normalere' aanpak van een DAO kun je in deze Engelstalige DAO tutorial lezen: DAO tutorial - the data layer.

Er zijn ook gratis danwel commerciële ORM's (Object Relational Mappers) verkrijgbaar die het vanuit Java werken met databases moet vergemakkelijken. Het bekendste voorbeeld is wel Hibernate. Bij een ORM komt het zo ongeveer erop neer dat je de database objectmatig via diverse hulpklassen kunt benaderen zonder handgeschreven SQL queries. Hibernate heeft helaas echter wel een enorm hoge leercurve, met name het kennis van XML is vereist en het op de juiste wijze configureren van de XML configuratie bestanden kan nogal lastig worden. Maar het is beslist wel de moeite waard.

Terug naar boven

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

(C) Maart 2007, BalusC

Thursday, February 1, 2007

Generics (Dutch)

Introductie

Generics, oftwel Generieken, die in Java 5.0 is geintroduceerd slaat op de harde typering van de gebruikte objecten in collecties, mappen, klassen en methoden. Het gebruik hiervan is niet verplicht, maar het kan uiteindelijk erg handig zijn. Stel je een ArrayList voor dat (volgens jouw code!) alleen maar Integer objecten kan bevatten:

package test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een algemene ArrayList.
        List arrayList = new ArrayList();

        // Voeg de Integer waarden toe.
        arrayList.add(new Integer("1"));
        arrayList.add(new Integer("2"));
        arrayList.add(new Integer("3"));

        // Doorloop de waarden.
        for (Iterator iter = arrayList.iterator(); iter.hasNext();) {

            // iter.next() retourneert een Object, je moet deze casten naar Integer.
            Integer integer = (Integer) iter.next();

            // Laat de waarde zien.
            System.out.println(integer);
        }
    }

}

1
2
3

Stel dat je al dan niet onbewust een String object aan de arrayList toevoegt, dan zou die cast naar Integer problemen opleveren: er wordt een ClassCastException afgeworpen. Een String kan namelijk niet gecast worden naar een Integer.

package test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een algemene ArrayList.
        List arrayList = new ArrayList();

        // Voeg de Integer waarden toe.
        arrayList.add(new Integer("1"));
        arrayList.add(new Integer("2"));
        arrayList.add("3"); // Let op: String ipv Integer.

        // Doorloop de waarden.
        for (Iterator iter = arrayList.iterator(); iter.hasNext();) {

            // iter.next() retourneert een Object, je moet deze casten naar Integer.
            Integer integer = (Integer) iter.next();

            // Laat de waarde zien.
            System.out.println(integer);
        }
    }

}

1
2
Exception in thread "main" java.lang.ClassCastException: java.lang.String
    at test.Test.main(Test.java:23)

De foutmelding spreekt voor zich: je kunt een object van het type java.lang.String niet casten naar een Integer.

Met Generics kun je dit soort problemen dus vroeg afvangen door alvast in de code de ArrayList te aanmerken als een lijst dat alleen maar objecten van het Integer type mag bevatten. De Java compiler zal dat detecteren en zal indien nodig alvast een compilatie fout melden wanneer je een object van het verkeerde type aan de lijst toevoegt. Dit gebeurt in principe dus vóórdat je de code kunt uitvoeren.

Terug naar boven

Generieke collecties

Wanneer je Generics wilt toepassen in collecties, dan moet je ze tussen de vishaken toevoegen aan de declaratie en instantiatie van het object. Hier heb je enkele voorbeelden:

List<Type> arrayList = new ArrayList<Type>();

Set<Type> treeSet = new TreeSet<Type>();

Het Type moet minimaal overeenkomen met het object type van de inhoud van de collectie. Minimaal, dus je kunt ook de supertype opgeven. In het geval van een Integer kun je dus gewoon Integer opgeven, of diens superklasse Number of zelfs heel drastisch de hoofdklasse Object. Wanneer je Integer opgeeft, dan kun je alleen objecten van dezelfde type of objecten van diens (op het moment niet-bestaande) subklasse in de collectie zetten. In het geval van Number kun je dus alleen objecten van de types Number, Integer, Long, Short, etc in de collectie stoppen. De officiële subklassen van Number kun je in de API documentatie vinden onder "Direct Known Subclasses". Wanneer je de type Object opgeeft, dan kun je wel raden dat je élk mogelijk type in de collectie kunt stoppen, wat de collectie eigenlijk ongeneriek zal maken ;)

We passen de oorsronkelijke code even aan met de Generics:

package test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een ArrayList voor Integers.
        List<Integer> integers = new ArrayList<Integer>();

        // Voeg de Integer waarden toe.
        integers.add(new Integer("1"));
        integers.add(new Integer("2"));
        integers.add(new Integer("3"));

        // Doorloop de waarden.
        for (Iterator<Integer> iter = integers.iterator(); iter.hasNext();) {

            // Dankzij de Generics is een cast niet nodig. De compiler weet in dit 
            // geval immers al van te voren uit welke object type de collectie bestaat.
            Integer integer = iter.next();

            // Laat de waarde zien.
            System.out.println(integer);
        }
    }

}

1
2
3

En nu proberen we een ordinaire String object aan de lijst te toevoegen:

        ...

        // Voeg de Integer waarden toe.
        integers.add(new Integer("1"));
        integers.add(new Integer("2"));
        integers.add("3"); // Let op: String ipv Integer.

        ...

The method add(Integer) in the type List<Integer> is not applicable for the arguments (String)

Deze code zal dus niet compileren en de bovenstaande compilatiefout teruggeven en deze code zal derhalve niet zomaar uitgevoerd kunnen worden. In een goede IDE, zoals Eclipse, krijg je ook een foutmelding te zien bij de add() methode.

Wanneer je de object type Number opgeeft, die zelf ook een tal subklassen heeft, dan kun je zonder problemen verschillende object typen van de superklasse Number in de collectie stoppen.

package test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een ArrayList voor Numbers.
        List<Number> nummers = new ArrayList<Number>();

        // Voeg de Number waarden toe.
        nummers.add(new Integer("1"));
        nummers.add(new Long("2"));
        nummers.add(new Short("3"));

        // Doorloop de waarden.
        for (Iterator<Number> iter = nummers.iterator(); iter.hasNext();) {

            // Dankzij de Generics is een cast niet nodig. De compiler weet in dit 
            // geval immers al van te voren uit welke object type de collectie bestaat.
            Number nummer = iter.next();

            // Laat de waarde zien.
            System.out.println(nummer);
        }
    }

}

1
2
3

Sinds Java 5.0 is er naast de bestaande Iterator een nieuwe manier om de collectie te doorlopen, namelijk die met de for statement voor array's. We passen de bovenstaande code even hierop aan:

        ...

        // Doorloop de nummers.
        for (Number nummer : nummers) {

            // Laat de nummer zien.
            System.out.println(nummer);
        }

        ...

De in de for statement gedeclareerde type moet minimaal overeenkomen met het object type van de inhoud van de collectie. Je zou boven Number dus ook Object kunnen opgeven. Het is maar net wat je met de doorgelopen waarden wilt doen.

Terug naar boven

Generieke mappen

Je kunt de Generics ook toepassen in mappen, hierbij volg je dezelfde richtlijnen als bij generieke collecties, met het verschil dat je zowel de sleutel als de waarde moet definiëren bij de declaratie en instantiatie van het object. Hier heb je enkele voorbeelden:

Map<SleutelType, WaardeType> hashMap = new HashMap<SleutelType, WaardeType>();

Map<SleutelType, WaardeType> hashtable = new Hashtable<SleutelType, WaardeType>();

Het SleutelType moet dus minimaal overeenkomen met het object type van de sleutels van de map en het WaardeType moet dus minimaal overeenkomen met het object type van de waarden van de map. Voor de rest gelden dezelfde richtlijnen als bij de collecties.

Even wat voorbeeldcode:

package test;

import java.util.LinkedHashMap;
import java.util.Map;

public class Test {

    public static void main(String[] args) {

        // Declareer en initialiseer een LinkedHashMap voor Integers
        // met een String sleutel.
        Map<String, Integer> map = new LinkedHashMap<String, Integer>();

        // Voeg de Integer waarden met String sleutels toe.
        map.put("Een", new Integer("1"));
        map.put("Twee", new Integer("2"));
        map.put("Drie", new Integer("3"));

        // Doorloop de sleutels.
        for (String sleutel : map.keySet()) {

            // Laat de sleutel en de waarde zien.
            System.out.println(sleutel + ", " + map.get(sleutel));
        }
    }

}

Een, 1
Twee, 2
Drie, 3

Terug naar boven

Generieke klassen

Je kunt de Generics ook toepassen in klassen. Dit gaat een stuk verder dan het toepassen van de Generics in collecties en mappen. Er wordt namelijk niet gekeken naar de directe subklassen of superklassen van het opgegeven type. Dit moet je zelf aangeven met behulp van de extends respectievelijk super sleutelwoorden.

Class<Type> c = Klasse van de gegeven type

Class<? extends Type> c = (Sub)klasse van de gegeven type

Class<? super Type> c = (Super)klasse van de gegeven type

Class<?> c = Willekeurige klasse

Hieronder volgen enkele voorbeelden, compleet met gesimuleerde fouten (rood onderstreept):

// Declareer en instantieer een klasse waarvan de type
// strikt gelijk aan de gegeven type is.
Class<Integer> c1 = Number.class; // Number is niet gelijk aan Integer.
Class<Number> c2 = Number.class;
Class<Object> c3 = Number.class; // Number is niet gelijk aan Object.

// Declareer en instantieer een klasse waarvan de type
// gelijk aan of een subklasse van de gegeven type is.
Class<? extends Integer> c4 = Number.class; // Number is geen subklasse van Integer.
Class<? extends Number> c5 = Number.class;
Class<? extends Object> c6 = Number.class;

// Declareer en instantieer een klasse waarvan de type
// gelijk aan of een superklasse van de gegeven type is.
Class<? super Integer> c7 = Number.class;
Class<? super Number> c8 = Number.class;
Class<? super Object> c9 = Number.class; // Number is geen superklasse van Object.

// Declareer en instantieer een klasse waarvan de type
// willekeurig kan zijn.
Class<?> c10 = Number.class;

Type mismatch: cannot convert from Class<Number> to Class<Integer>
Type mismatch: cannot convert from Class<Number> to Class<Object>
Type mismatch: cannot convert from Class<Number> to Class<? extends Integer>
Type mismatch: cannot convert from Class<Number> to Class<? super Object>

Terug naar boven

Generieke methoden

Je kunt de Generics ook toepassen in de parameters en de return type van de methoden. Hierbij kun je dezelfde richtlijnen volgen als bij de generieke klassen. Hier heb je enkele voorbeelden op basis van een List:

package test;

import java.util.ArrayList;
import java.util.List;

public class Test {

    public void methode1(List<Number> nummers) {

        // De parameter van deze methode accepteert dus alleen de volgende lijst:
        List<Number> nummers;
    }

    public void methode2(List<? extends Number> nummers) {

        // De parameter van deze methode accepteert dus o.a. de volgende lijsten:
        List<Number> nummers;
        List<Integer> integers; // Integer is een subklasse van Number.
        List<Long> longs; // Long is een subklasse van Number.
        List<Short> shorts; // Short is een subklasse van Number.
    }

    public void methode3(List<? super Number> nummers) {

        // De parameter van deze methode accepteert dus alleen de volgende lijsten:
        List<Number> nummers;
        List<Object> objecten; // Object is een superklasse van Number.
    }

    public List<Number> methode4() {

        // Deze methode kan dus alleen de volgende lijst retourneren:
        List<Number> nummers = new ArrayList<Number>();

        return nummers;
    }

    public List<? extends Number> methode5() {

        // Deze methode kan dus o.a. de volgende lijsten retourneren:
        List<Number> nummers = new ArrayList<Number>();
        List<Integer> integers; // Integer is een subklasse van Number.
        List<Long> longs; // Long is een subklasse van Number.
        List<Short> shorts; // Short is een subklasse van Number.

        return nummers;
    }

    public List<? super Number> methode6() {

        // Deze methode kan dus alleen de volgende lijsten retourneren:
        List<Number> nummers = new ArrayList<Number>();
        List<Object> objecten; // Object is een superklasse van Number.

        return nummers;
    }

}
Terug naar boven

Generieke typen

Tenslotte kun je ook generieke typen gebruiken in eigengemaakte klassen. Dan kun je jouw klassen op dezelfde manier gebruiken als collecties en mappen. Dit kan vooral handig zijn als je veel bezig bent met abstracte klassen en polymorphisme. Om een generieke type binnen een klasse te kunnen gebruiken, moet je deze in de declaratie van de klasse hebben opgenomen:

public class Klasse<Type*> {}

Je kunt meerdere typen opgeven, gescheiden door een komma. Hieronder volgt een voorbeeld van een klasse met twee generieke typen:

package test;

public class DataTransferObject<T1, T2> {

    private T1 data1;
    private T2 data2;

    public void add(T1 data1, T2 data2) {
        this.data1 = data1;
        this.data2 = data2;
    }

    public T1 getData1() {
        return data1;
    }

    public T2 getData2() {
        return data2;
    }

}

Binnen eigen klassen kun je de typen inderdaad een willekeurige naam geven, zoals T1, T2 of eventueel Type1, Type2 of zelfs A, B, C, etcetera. Het maakt niet uit hoeveel typen je opgeeft. Let wel, wanneer je de naam van een bestaande klasse gebruikt, zoals Integer, String, etc, dan wordt de bestaande klasse overgeschreven met de gegeven type!

Zo'n klasse kan dan bijvoorbeeld als volgt worden gebruikt, compleet met een gesimuleerde foutmelding:

public void test() {

    // Declareer en initialiseer een DTO dat een String en een Integer kan bevatten.
    DataTransferObject<String, Integer> dto1 = new DataTransferObject<String, Integer>();
    dto1.add("Een", new Integer("1"));

    // Declareer en initialiseer een DTO dat een String en een Integer kan bevatten.
    DataTransferObject<String, Integer> dto2 = new DataTransferObject<String, Integer>();
    dto2.add(1, "Een"); // Fout: de types komen niet overeen.
}

The method add(String, Integer) in the type DataTransferObject<String,Integer> is not applicable for the arguments (int, String)

Je kunt net als bij generieke klassen en methoden ook subklassen toestaan in de typering, gebruikmakend van de extends bepaling in de typering. Je mag volgens de specificatie echter geen superklassen (super) toestaan in de typering. Je kunt ook geen geen wildcard (?) gebruiken om een willekeurige object type mee aan te geven. Da's immers zinloos, aangezien je je eigen typen een eigen naam kunt meegeven.

Hieronder volgt een voorbeeld van de extends bepaling in de typering:

public class DataTransferObject<T1 extends Number, T2> {

}

Je kunt deze op dezelfde manier gebruiken als bij generieke klassen en methoden (let ook op de gesimuleerde fouten):

public void test() {

    // Declareer en initialiseer enkele DTO's.
    DataTransferObject<Number, Object> dto1 = new DataTransferObject<Number, Object>();
    DataTransferObject<Integer, String> dto2 = new DataTransferObject<Integer, String>();
    DataTransferObject<String, Integer> dto3 = new DataTransferObject<String, Integer>();

}

Bound mismatch: The type String is not a valid substitute for the bounded parameter <T1 extends Number> of the type DataTransferObject<T1,T2>
Bound mismatch: The type String is not a valid substitute for the bounded parameter <T1 extends Number> of the type DataTransferObject<T1,T2>

Ja, deze foutmelding krijg je in dit geval inderdaad 2x ;)

Wanneer je de generieke typering per methode wilt declareren, dan moet je deze vóór het return type in de declaratie vaststellen. Ook hier mag je geen superklassen toestaan in de typering.

public <Type> void methode() {}

Hier heb je enkele voorbeelden op basis van een List:

package test;

import java.util.ArrayList;
import java.util.List;

public class Test {

    public <T extends Number> void methode7(List<T> nummers) {

        // De parameter van deze methode accepteert dus alleen de volgende lijst:
        List<T> nummerTypes;

        // Binnen de methode kun je de T verder gebruiken als subklasse van Number.
    }

    public <T extends Number> List<T> methode8() {

        // Deze methode kan dus alleen de volgende lijst retourneren:
        List<T> nummerTypes = new ArrayList<T>();

        return nummerTypes;

        // Binnen de methode kun je de T verder gebruiken als subklasse van Number.
    }

    public <T extends Number> List<T> methode9(Class<T> klasse) {

        // De parameter van deze methode accepteert dus alleen de volgende klasse:
        Class<T> c;

        // Deze methode kan dus alleen de volgende lijst retourneren:
        List<T> nummerTypes = new ArrayList<T>();

        return nummerTypes;

        // Binnen de methode kun je de T verder gebruiken als subklasse van Number.
    }

}

Wanneer je de definitie van de type uit de declaratie weglaat, terwijl de type ook niet in de klasse is gedeclareerd, dan zul je de volgende foutmelding krijgen:

package test;

import java.util.List;

public class Test {

    public void methode10(List<T> nummers) {

    }

}

T cannot be resolved to a type

Terug naar boven

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

(C) Januari 2007, 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