Algoritmos y Estructuras de Datos Herramientas Lenguaje de programación
!Prog C/C++ Rust
Linux Matemáticas
Mates Discretas
Programación Orientada a Objetos Sistemas Operativos

Encapsulación

[date: 13-01-2024 19:07] [last modification: 15-01-2024 22:24]
[words: 2379] [reading time: 12min] [size: 62022 bytes]

En este primer artículo, se verá el concepto de Encapsulación, Clase y Objeto. Además, se explorarán los constructores, getters, setters y métodos funcionales.

Teorema del Programa Estructurado

Es posible crear cualquier función computable con 3 estructuras lógicas (llamadas estructuras de control):

  • secuencial
  • condicional (if)
  • iterativo (for, while)

Más información: Wikipedia

Todo programa se puede entender como una serie de instrucciones que operan sobre un conjunto de datos de entrada y generan otro conjunto de datos de salida. Por eso, los tipos de datos son conceptos fundamentales de un lenguaje de programación.

En Java:

Encapsulación

Encapsulación

La encapsulación es una forma de abstracción de datos, que consiste en la agrupación unos datos con los métodos (funciones) que los manipulan. Estos especifican los estados posibles y las operaciones permitidas.

Se utiliza para ocultar los valores o estados internos, evitando el acceso directo a ellos por el resto del programa, porque de lo contrario, se podrían dar errores de estados inválidos. Los métodos actúan de intermediarios para que esto no ocurra.

El usuario utiliza los métodos que tiene accesibles para operar sobre los datos, sin preocuparse por su representación interna ni otros detalles de implementación, a modo de caja negra. Lo importante es qué hacen, no cómo lo hacen.

Es la característica fundamental de la Programación Orientada a Objetos.

Este concepto facilita mucho la modularidad de los programas y reduce los errores.

Clases y objetos

Clase

Una clase es una plantilla para la creación de objetos. Define unas características comunes mediante unas variables llamadas atributos y una serie de acciones mediante funciones llamados métodos.

Están vinculados a entidades de alto nivel del programa, y se utilizan tipos de datos primitivos para representarlas.

Objeto
Un objeto es una instancia concreta de una clase. Esto implica que el objeto tiene una dirección de memoria concreta y un valor para cada uno de los atributos.

Coloquialmente, una clase es un struct con una colección de funciones que operan sobre ella, y cuando el struct está en memoria, se le llama objeto.

Lo mejor es ver un ejemplo:

La clase Persona tiene:

 1public class Persona {
 2    private String nombre;
 3    private int edad;
 4
 5    public Persona(String nombre, int edad) {
 6        this.nombre = nombre;
 7        this.edad = edad;
 8    }
 9
10    public void presentarse() {
11        System.out.printf("Hola! Me llamo %s ya tengo %d años\n", nombre, edad);
12    }
13
14    // ...
15}

Entonces:

Persona pepe = new Persona("Pepe Fernández", 27);
pepe.presentarse();
// Imprime: "Hola! Me llamo Pepe Fernández y tengo 27 años"

pepe es un objeto de la clase Persona. Tiene un nombre concreto (Pepe Fernández) y una edad concreta (27). No confundir el nombre de variable que se le ha puesto (pepe) con el atributo de nombre.

Estructura de una clase en Java

 1public <modificador clase> class <NombreClase> {
 2    // Atributos
 3    <tipo acceso> [<modificador atributo>] <TipoDato> <nombreAtributo>;
 4
 5    // Constructor
 6    public <NombreClase> (<argumentos>) {
 7       // ...
 8    }
 9
10    // Getters
11    public <TipoAtributo> get<NombreAtributo>() {}
12        return <nombreAtributo>;
13    }
14
15    // Setters
16    public void set<NombreAtributo>(<TipoAtributo> <nombreAtributo>) {
17        // Comprobación del argumento
18        this.<nombreAtributo> = <nombreAtributo>;
19    }
20
21    // Métodos funcionales
22    <modificador acceso> [<modificador método>] <TipoDato>
23    <nombreMétodo>(<TipoDado> <nombreParam>, ...) [throws <NombreExcepción>]
24    {
25        // ...
26    }
27}

Donde <modificador clase> uno de:

Y <modificador atributo> y <modificador método> puede ser cualquier número de (hay algunos que colisionan y no pueden ir juntos, el compilador dará un error):

Solo para <modificador atributo>:


Para llamar a un método se usa la sintaxis <objeto>.<método>(<params>), y para acceder a un atributo <objeto>.<atributo>.

Nótese que un atributo es una variable de ámbito de clase (todos los métodos tienen acceso), si se declara una variable se declara dentro de un método, solo será local al método, por lo que el resto de no lo verán y se borrará cuando termine.

Tipos de acceso en Java

Tipo de accesoPalabra claveClasePaqueteSubclaseOtros
Públicopublic
Protegidoprotected
Acceso a paquete
Privadoprivate
Buenas prácticas
  • Los atributos de una clase siempre deben ser privados, de lo contrario se pierde la encapsulación. Para acceder y modificar los datos, se usan los getters y setters.
  • Los métodos pueden ser públicos o privados. Un método privado sirve de ayuda a otros métodos.
  • Los setters deben incluir comprobaciones para escribir valores erróneos como null.
  • Todo atributo debe tener su getter y setter, salvo casos donde el atributo sea solo lectura o interno a la clase.

Métodos de clase

Constructores

Para tipos de datos que no son clases, los tipos de datos primitivos, se realiza una reserva de memoria y se inicia con su valor por defecto de forma automática.

TipoValor inicial
booleanfalse
char'\0'
byte short int long0
float double+0.0

Sin embargo, para crear objetos, no se reservará memoria automáticamente, dado que se desconoce cuánto ocupará. En su lugar, se asigna el valor null por defecto, por lo que si se intenta usar dará un NullPointerException (similar a un Segmentation Fault).

Persona pepe; // Aquí pepe es null

Como el compilador ya sabe que contiene el valor null, da error: variable pepe might not have been initialized cuando se intente llamar a algún método.

La reserva de memoria se hace con el operador new y llamando al constructor.

Persona pepe = new Persona("Pepe Fernández", 27);
//             ~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//     Uso de new   Llamada al constructor
Constructor

Un constructor es un método especial que se invoca una única vez por cada objeto (si se llama dos veces, se crean dos objetos distintos) para:

  • Reservar memoria para el objeto
  • Reservar memoria para los atributos
  • Asignar los valores iniciales a los atributos (desde parámetro o valores por defecto)

Nunca devuelve nada y se llama igual que la clase.

Constructor por defecto

Si el programador no ha creado de forma explícita un constructor, Java automáticamente añade el constructor por defecto.

  • No tiene argumentos
  • Inicializa todo por defecto: clases a null y datos primitivos según la tabla anterior.

Por tanto, es equivalente a añadir el siguiente código:

public <NombreClase>() {}

Si no fuese por este mecanismo, nunca sería posible crear instancias de esta clase.

Buenas prácticas
Siempre se debe crear un constructor e iniciar todos los atributos.

Getters y setters

Los getters (de la palabra get), devuelven el valor que tienen los atributos en cada momento, llamados por convención get<NombreAtributo>.

Los setters (de la palabra set), escriben en valor que se pasa por parámetros en el atributo, siempre y cuando cumplan determinadas condiciones (no ser null, estar entre un rango de valores, etc). Se llaman por convención set<NombreAtributo>.

Solo hay como máximo un único getter y un único setter por cada atributo. Esta es la única forma que tiene el usuario de la clase para acceder/modificar al atributo, por lo que si es programador decide que no quiere que se modifique cierto valor, simplemente no añade un setter.

Métodos funcionales

Son el resto de métodos que implementan la funcionalidad de la clase. Pueden acceder directamente sobre los atributos de la clase, no hace falta que se use también los getters/setters.

Métodos (y atributos) estáticos

La palabra clave static se puede usar tanto en atributos como en métodos, y significa propio de la clase y no del objeto. Esto requiere que el método o atributo esté siempre en memoria estática por si se usa, a diferencia del resto que requieren de un objeto: cuando se carga el objeto, se trae consigo sus métodos y se reserva memoria para sus atributos.

Para métodos, esto quiere decir que se puede invocar sin haber creado un objeto de la clase. Esto implica que no se puedan hacer llamadas a métodos no estáticos desde un método estático:

1public class Consola {
2    public static void imprimir(String msg) {
3        System.out.printf("Método estático: %s\n", msg);
4    }
5}
6
7// ...
8Consola.imprimir("HOLA");

Para atributos, el valor se conserva independientemente del objeto en el que se utilice. Esto resulta útil para mantener un estado global entre todos, como para hacer un generador de identificadores:

1public class Jugador {
2    public static int ultimoId = 0;
3    public final int id;
4
5    public Jugador() {
6        ultimoId++;
7        this.id = ultimoId;
8    }
9}

Sobrecarga de métodos

Se pueden tener varios métodos con el mismo nombre, siempre y cuando tengan parámetros distintos (no aplica a valor de retorno).

public void cargarDatos(File archivo) { ... }
public void cargarDatos(Dato dato) { ... }
public void cargarDatos(Arraylist<Dato> datos) { ... }

Condiciones de uso:

Métodos especiales

Método toString

Todas las clases de Java heredan de la clase Object, que implementa el método toString. Esta devuelve una representación en texto de un objeto. Es posible reimplementar este método en cada subclase para cambiar el funcionamiento por defecto (muestra el nombre de la clase junto con un código, lo que no es muy útil):

@Override
public String toString() {
    return """
           {
               nombre: %s
               edad: %d
           }
           """.formatted(this.nombre, this.edad);
}

En general nos interesa que todas las clases deriven este método para describir el objeto incluyendo los datos de los atributos más relevantes.

Método equals

¿Cómo consideramos que dos métodos son iguales?

Para esta segunda opción, hay que comprobar 3 condiciones:

 1@Override
 2public boolean equals(Object obj) {
 3    // Comprobar la dirección de memoria
 4    if (this == obj) {
 5        return true;
 6    }
 7
 8    // Comprobar que no sea null
 9    if (obj == null) {
10        return false;
11    }
12
13    // Comprobar que sea del mismo tipo
14    if (this.getClass() != obj.getClass()) {
15        return false;
16    }
17
18    <NombreClase> <nombre> = (<NombreClase>) obj;
19    return <criterio de igualdad>;
20}

Dicho criterio de igualdad debe hacerse sobre atributos de valor inmutable, que no se puedan modificar usando aliasing.

Además, este método se usa internamente en las colecciones de datos (contains y otros), por lo que al implementarlo se reutiliza mucho código.

Buena práctica
Se debe implementar siempre el método equals.

Método hashCode

Este método devuelve un entero generado un algoritmo de hash, que necesitan ciertas estructuras de datos como el HashMap o el HashSet. La implementación por defecto del método es específico a la JVM que se esté usando.

Nótese que return 1 es un método hashCode que cumple todos los requisitos, pero no es particularmente bueno. Cuanto mejor sea la función hash, mejor será el rendimiento de las estructuras de datos que lo utilicen.

@Override
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode();
}

Existen muchas formas de implementar un una función hash, unas mejores que otras. En este link puede encontrar algunos ejemplos.

Método main

El programa comienza llamando al método main (análogo a lenguajes como C, que inician en la función main) y debe tener muy poco código: debe limitarse a crear una clase que se encargará de todo.

1public class MainClass {
2    public static void main(String[] args) {
3        // Código
4    }
5}

La clase principal también debe ser muy pequeña y no tener atributos.

Registros

En Java 14 se introdujo los records, clases inmutables que carecen de setters, por lo que no se puede modificar los valores de sus atributos en tiempo de ejecución. Tienen las siguientes restricciones:

 1public record Persona(String nombre, int edad) {
 2    // Se puede reimplementar el constructor canónico para añadir chequeos
 3    // y otras operaciones relevantes
 4    public Persona {
 5        Objects.requireNonNull(nombre); // assert nombre != null
 6        System.out.println(nombre + " creado");
 7    }
 8
 9    // Se pueden añadir constructores adicionales que llaman al canónico
10    public Persona(String nombre) {
11        this(nombre, 18);
12    }
13
14    // Métodos funcionales que no modifiquen los atributos
15    // ...
16}

Su gran ventaja es que ahorra al programador tener que escribir mucho código para producir este mismo resultado.

Volver a Programación Orientada a Objetos Siguiente: Referencias