Métodos de objetos de Java: finalizar ()

Este artículo es la continuación de una serie de artículos que describen los métodos a menudo olvidados de la clase de objeto base del lenguaje Java. Los siguientes son...

Introducción

Este artículo es una continuación de una serie de artículos que describen los métodos a menudo olvidados de la clase de objeto base del lenguaje Java. Los siguientes son los métodos del Objeto base de Java que están presentes en todos los objetos de Java debido a la herencia implícita de Objeto.

El enfoque de este artículo es el método Object#finalize() que se utiliza durante el proceso de recolección de elementos no utilizados internamente por Java Virtual Machine (JVM). Tradicionalmente, el método ha sido anulado por las subclases de Object cuando la instancia de la clase necesita cerrar o purgar los recursos del sistema, como las conexiones de la base de datos y los controladores de archivos. Sin embargo, los expertos en lenguaje Java han argumentado durante mucho tiempo que anular finalize() para realizar operaciones como la destrucción de recursos no es una buena idea.

De hecho, los documentos de Oracle Java establecen que el método finalize() en sí mismo ha quedado obsoleto, por lo que se ha etiquetado para eliminarlo en versiones futuras del lenguaje, ya que los mecanismos subyacentes para la creación de objetos y la recolección de elementos no utilizados han estado bajo reevaluación. Recomiendo encarecidamente seguir el consejo de dejar el método finalize() sin implementar.

Además, quiero dejar en claro que el objetivo principal de este artículo es brindar orientación para migrar el código existente que implementa finalize() a la construcción preferida de implementar la interfaz AutoClosable junto con la construcción emparejada de prueba con recursos. introducido en Java 7.

Ejemplo de implementación de Finalize

Aquí hay un ejemplo que puede ver en algún código heredado donde finalize se anuló para proporcionar la funcionalidad de limpiar un recurso de base de datos cerrando una conexión a una base de datos SQLite dentro de una clase llamada PersonDAO. Tenga en cuenta que este ejemplo utiliza SQLite, que requiere un controlador de conector de conectividad de base de datos Java (JDBC) de terceros, que deberá descargarse de aquí y agregarse al classpath si quieres seguirlo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class MainFinalize {

    public static void main(String[] args) {
        try {
            PersonDAO dao = new PersonDAO();
            Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
            dao.create(me);
        } catch(SQLException e) {
            e.printStackTrace();
        }
    }
    
    /* PersonDAO implementing finalize() */
    static class PersonDAO {
        private Connection con;
        private final Path SQLITE_FILE = Paths.get(System.getProperty("user.home"), "finalize.sqlite3");
        private final String SQLITE_URL = "jdbc:sqlite:" + SQLITE_FILE.toString();
        
        public PersonDAO() throws SQLException {
            con = DriverManager.getConnection(SQLITE_URL);
            
            String sql = "CREATE TABLE IF NOT EXISTS people ("
                    + "id integer PRIMARY KEY,"
                    + "first_name text,"
                    + "last_name text,"
                    + "dob text);";
            Statement stmt = con.createStatement();
            stmt.execute(sql);
        }
        
        void create(Person person) throws SQLException {
            String sql = "INSERT INTO people (first_name, last_name, dob) VALUES (?, ?, ?)";
            PreparedStatement stmt = con.prepareStatement(sql);
            stmt.setString(1, person.getFirstName());
            stmt.setString(2, person.getLastName());
            DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            stmt.setString(3, person.getDob().format(fmt));
            stmt.executeUpdate();
        }
        
        @Override
        public void finalize() {
            try {
                con.close();
            } catch(SQLException e) {
                System.out.println("Uh, oh ... could not close db connection");
            }
        }
    }
    
    /* Simple Person data class */
    static class Person {
        private final String firstName;
        private final String lastName;
        private final LocalDate dob;
        
        Person(String firstName, String lastName, LocalDate dob) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.dob = dob;
        }
        
        String getFirstName() {
            return firstName;
        }

        String getLastName() {
            return lastName;
        }

        LocalDate getDob() {
            return dob;
        }
    }
}

Como mencioné anteriormente, este no es el método preferido para cerrar un recurso y, de hecho, debe desaconsejarse encarecidamente. En su lugar, se debe implementar un código similar al del método PersonDAO#finalize dentro del método AutoClosable#close como se muestra a continuación en el siguiente ejemplo.

Una mejor solución: prueba con recursos y autocierre

Java 7 introdujo la interfaz AutoCloseable junto con una mejora en la construcción tradicional try / catch que proporciona una solución superior para limpiar los recursos que se encuentran en un objeto. Al usar esta combinación de AutoClosable y probar con recursos, el programador tiene un mayor control sobre cómo y cuándo se liberará un recurso, lo que a menudo era impredecible cuando se anulaba el método Object#finalize().

El ejemplo que sigue toma el PersonDAO anterior e implementa la interfaz AutoCloseable#close para cerrar la conexión a la base de datos. Luego, el método principal utiliza la construcción try-with-resources en lugar del anterior try/catch para manejar la limpieza.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class MainFinalize {

    public static void main(String[] args) {
        try (PersonDAO dao = new PersonDAO()) {
            Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
            dao.create(me);
        } catch(SQLException e) {
            e.printStackTrace();
        }
    }
    
    /* PersonDAO implementing finalize() */
    static class PersonDAO implements AutoCloseable {
        private Connection con;
        private final Path SQLITE_FILE = Paths.get(System.getProperty("user.home"), "finalize.sqlite3");
        private final String SQLITE_URL = "jdbc:sqlite:" + SQLITE_FILE.toString();
        
        public PersonDAO() throws SQLException {
            con = DriverManager.getConnection(SQLITE_URL);
            
            String sql = "CREATE TABLE IF NOT EXISTS people ("
                    + "id integer PRIMARY KEY,"
                    + "first_name text,"
                    + "last_name text,"
                    + "dob text);";
            Statement stmt = con.createStatement();
            stmt.execute(sql);
        }
        
        void create(Person person) throws SQLException {
            String sql = "INSERT INTO people (first_name, last_name, dob) VALUES (?, ?, ?)";
            PreparedStatement stmt = con.prepareStatement(sql);
            stmt.setString(1, person.getFirstName());
            stmt.setString(2, person.getLastName());
            
            DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            stmt.setString(3, person.getDob().format(fmt));
            stmt.executeUpdate();
        }
        
        @Override
        public void close() {
            System.out.println("Closing resource");
            try {
                con.close();
            } catch(SQLException e) {
                System.out.println("Uh, oh ... could not close db connection");
            }
        }
    }
    
    /* Simple Person data class */
    static class Person {
        // everything remains the same here ...
    }
}

Vale la pena explicar el contrato de prueba con recursos con un poco más de detalle. Esencialmente, el bloque de prueba con recursos se traduce en un bloque completo de prueba / captura / finalmente como se muestra a continuación, pero, con el beneficio de ser más limpio, cada vez que usa una clase que implementa AutoCloseable # close, usaría un intento- with-resource en lugar de volver a implementar el bloque finalmente en un intento / captura / finalmente como se muestra a continuación. Tenga en cuenta que la clase java.sql.Connection implementa AutoCloseable#close.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
try {
    Connection con = DriverManager.getConnection(someUrl);
    // other stuff ...
} catch (SQLException e) {
    // logging ... 
} finally {
   try {
        con.close();
   } catch(Exception ex) {
        // logging ...
   }
}

Sería mejor implementarlo así:

1
2
3
4
5
try (Connection con = DriverManager.getConnection(someUrl)) {
    // do stuff with con ...
} catch (SQLException e) {
    // logging ... 
}

Conclusión

En este artículo, describí a propósito el método Object#finalize() de manera fugaz debido al hecho de que no se sugiere que uno deba implementarlo. Para contrastar la falta de profundidad gastada en el método finalize(), he descrito un enfoque preferible para resolver el problema de la limpieza de recursos usando el dúo AutoClosable y try-with-resources.

Como siempre, gracias por leer y no se avergüence de comentar o criticar a continuación.

Licensed under CC BY-NC-SA 4.0