Saltar al contenido principal

Programación orientada a objetos (OOP)

Hasta ahora hemos trabajado principalmente con variables, condiciones, bucles, arrays y cadenas.

A medida que los programas crecen, puede resultar complicado organizar toda la información utilizando únicamente variables y funciones.

La Programación Orientada a Objetos (POO) es un paradigma de programación que permite organizar el código utilizando clases y objetos.

La idea principal es representar dentro del programa diferentes elementos que tienen:

  • Datos, que llamaremos atributos.
  • Comportamientos/Acciones, que representaremos mediante métodos.

Por ejemplo, si estamos creando un programa para gestionar una tienda, podríamos tener objetos como:

Producto
Cliente
Pedido

Cada uno de estos objetos podría tener sus propios datos y comportamientos.

Un Producto podría tener:

nombre
precio
stock

Y podría tener métodos/acciones como:

mostrarInformacion()
vender()

De esta forma, la información relacionada con cada elemento queda organizada.

Conceptos clave

  1. Clase: Una clase es una plantilla o molde que define la estructura y el comportamiento de los objetos a representar. Por ejemplo, una clase Coche podría definir cómo se crean, qué caracaterísticas tienen y cómo se comportan los objetos tipo coche.
  2. Objeto: Un objeto es una instancia concreta de una clase. Representa un ente en concreto que puede tener atributos (datos) y comportamientos (métodos) de su clase. Por ejemplo, un objeto podría ser una instancia de la clase Coche llamada "coche1" con valores específicos en los atributos de la clase como color "rojo" y modelo "Audi Q2", y métodos como acelerar y frenar.
  3. Atributos: Los atributos son variables que almacenan datos en un objeto. Cada objeto tiene su propio conjunto de valores de atributos. En el ejemplo del coche, los atributos podrían ser "color" y "modelo".
  4. Métodos: Los métodos son funciones que definen el comportamiento de un objeto. Los objetos pueden invocar métodos para realizar acciones o calcular valores. En el ejemplo del coche, los métodos podrían ser "acelerar" y "frenar".
  5. Encapsulación: La encapsulación es el principio de ocultar los detalles internos de una clase y proporcionar una interfaz pública para interactuar con los objetos. Esto promueve la seguridad y el mantenimiento del código. Lo veremos al final de la lección.

Beneficios

  1. Reutilización de Código: La POO permite reutilizar código a través de la creación de clases y la creación de objetos a partir de esas clases. Esto reduce la duplicación de código y facilita el mantenimiento.
  2. Abstracción: Permite modelar conceptos del mundo real en un software de manera más natural y abstracta. Los objetos y clases reflejan objetos y conceptos reales.
  3. Modularidad: El código se divide en objetos y clases, lo que facilita la organización y comprensión del software. Los cambios en una clase tienen un impacto limitado en el resto del programa.
  4. Herencia: La POO permite crear nuevas clases basadas en clases existentes, heredando sus atributos y métodos. Esto promueve la reutilización y la extensión del código.
  5. Polimorfismo: Permite que los objetos de diferentes clases se comporten de manera similar al definir métodos con el mismo nombre en clases diferentes. Facilita la gestión de objetos de manera genérica.

Clases y objetos

Antes de empezar a crear clases y objetos es importante entender la diferencia entre ambos conceptos.

Una clase es como un molde o plantilla.

Un objeto es un elemento creado a partir de ese molde.

Podemos imaginar una clase como la definición de "ser persona".

¿Qué es ser una persona?

Por ejemplo podemos definir que toda persona tiene nombre, apellido, edad, color de ojos...

La clase Persona definiría todos esos atributos y comportamientos que para nosotros defininen a cualquier persona, pero no es ninguna persona en concreto.

A partir de la definición de Persona (la clase), podemos crear tantos objetos de dicha clase como queramos. Podemos crear el objeto "juan" dando valor a los atributos definidos en la clase:

nombre: Juan
apellido: García
edad: 35
color de ojos: marrón

En Java ocurre algo parecido:

Clase Persona

┌───┴────┐
↓ ↓
Persona 1 Persona 2 ...

La clase define qué características y comportamientos tendrá una persona.

Después podemos crear tantos objetos Persona como necesitemos.


Clases

Una clase se define utilizando la palabra clave class.

Por ejemplo:

public class Persona {

}

Acabamos de crear una clase llamada Persona en el fichero Persona.java.

Por sí sola, esta clase todavía no hace demasiado.

Dentro de ella podemos definir sus atributos y métodos.


Atributos

Los atributos son variables que pertenecen a un objeto.

Representan los datos o características del objeto.

Por ejemplo, una persona puede tener:

nombre
edad

Los atributos se definen nada más empezar la clase indicando tipo nombreVariable;.

public class Persona {

String nombre;
int edad;

}

Ahora nuestra clase Persona tiene dos atributos:

  • nombre
  • edad

Métodos

Los métodos representan comportamientos o acciones que puede realizar un objeto.

Por ejemplo, una persona podría:

saludar()
mostrarEdad()

Podemos definir estos métodos dentro de la clase:

public class Persona {

String nombre;
int edad;

public void saludar() {

System.out.println("Hola");

}

}

Ahora cualquier objeto de la clase Persona podrá utilizar el método saludar().


Objetos

Una clase es solamente una plantilla.

Para utilizarla necesitamos crear objetos.

Para crear un objeto utilizamos la palabra clave new. Generalmente los objetos concretos se crearán dentro del programa main.

Para crear un objeto de tipo Persona:

Persona persona1 = new Persona();

Podemos dividir esta línea en varias partes:

Persona persona1 = new Persona();
↓ ↓ ↓
Tipo Variable Crear objeto

Persona indica el tipo de objeto.

persona1 es el nombre de la variable que almacenará el objeto.

new Persona() crea una nueva instancia de la clase.


Crear varios objetos

Podemos crear tantos objetos como necesitemos.

Persona persona1 = new Persona();
Persona persona2 = new Persona();
Persona persona3 = new Persona();

Los tres objetos pertenecen a la misma clase:

Persona

├── persona1
├── persona2
└── persona3

Pero cada objeto tiene sus propios atributos.


Acceder a los atributos

Podemos acceder a los atributos de un objeto utilizando el operador ..

Por ejemplo:

Persona persona1 = new Persona();

persona1.nombre = "Ana";
persona1.edad = 25;

Ahora el objeto contiene:

persona1
├── nombre → "Ana"
└── edad → 25

Podemos consultar esos valores:

System.out.println(persona1.nombre);
System.out.println(persona1.edad);

Salida:

Ana
25

Acceder a los métodos

Del mismo modo que podemos acceder a los atributos, podemos llamar a los métodos utilizando ..

Por ejemplo:

persona1.saludar();

Esto ejecutará el método saludar() perteneciente al objeto persona1.

:pencil: NOTA: los métodos se llaman con paréntesis al final () mientras que los atributos solamente indicamos su nombre.


Constructores

Hasta ahora hemos creado objetos de esta forma:

Persona persona1 = new Persona();

Y posteriormente dábamos un valor concreto a sus atributos con el ..

Pero podemos hacer que, al crear un objeto, sea obligatorio proporcionar determinados datos.

Para ello utilizamos los constructores.

Un constructor es un método especial que se ejecuta automáticamente cuando creamos un objeto, permitiéndonos indicar en la creación del objeto los valores de los atributos tal que así:

Persona persona1 = new Persona("Anna", 25);
Persona persona2 = new Persona("Gema", 32);

El constructor se crea justo después de la definición de los atributos tal que así:

public class Persona {

String nombre;
int edad;

public Persona(String nombre, int edad) {

this.nombre = nombre;
this.edad = edad;

}

}

Su sintaxis es siempre:

public class NombreClase {

tipo1 atributo1;
tipo2 atributo2;

public NombreClase(tipo1 parametro1, tipo2 parametro2) {

this.atributo1 = parametro1;
this.atributo2 = parametro2;

}

}

Ahora veremos cómo funciona.


this

La palabra clave this hace referencia al objeto actual.

Puede resultar especialmente útil cuando los parámetros de un método o constructor tienen el mismo nombre que los atributos.

Observa:

public class Persona {

String nombre;
int edad;

public Persona(String nombre, int edad) {

this.nombre = nombre;
this.edad = edad;

}

}

Tenemos dos variables llamadas nombre:

this.nombre → atributo del objeto

nombre → parámetro del constructor

Por lo tanto:

this.nombre = nombre;

significa:

El atributo nombre del objeto
=
El parámetro nombre recibido

Al escribir:

Persona persona1 = new Persona("Anna", 25);

Es como decirle:

this.nombre = "Anna";
this.edad = 25;

¿Por qué necesitamos this?

Podríamos utilizar nombres diferentes:

public Persona(String nombrePersona, int edadPersona) {

nombre = nombrePersona;
edad = edadPersona;

}

Esto funcionaría correctamente.

Pero normalmente es más habitual utilizar el mismo nombre para el atributo y el parámetro:

public Persona(String nombre, int edad) {

this.nombre = nombre;
this.edad = edad;

}

De esta forma el código resulta más claro y fácil de mantener.


Métodos dentro de una clase

Una clase puede tener varios métodos.

Por ejemplo:

public class Persona {

// Atributos
String nombre;
int edad;

// Constructor
public Persona(String nombre, int edad) {

this.nombre = nombre;
this.edad = edad;

}

// Métodos
public void saludar() {

System.out.println("Hola, soy " + nombre);

}

public void mostrarInformacion() {

System.out.println("Nombre: " + nombre);
System.out.println("Edad: " + edad);

}

}

Podemos crear un objeto y utilizar sus métodos:

Persona persona1 = new Persona("Ana", 25);

persona1.saludar();

persona1.mostrarInformacion();

Salida:

Hola, soy Ana

Nombre: Ana
Edad: 25

Observa que dentro de los métodos podemos utilizar directamente los atributos del objeto:

public void saludar() {

System.out.println("Hola, soy " + nombre);

}

Métodos que devuelven valores

Los métodos también pueden devolver información.

Por ejemplo, podemos crear un método que devuelva el nombre:

public String obtenerNombre() {

return nombre;

}

Y otro que devuelva la edad:

public int obtenerEdad() {

return edad;

}

Podemos utilizar estos métodos desde el main:

Persona persona1 = new Persona("Ana", 25);

System.out.println(persona1.obtenerNombre());
System.out.println(persona1.obtenerEdad());

Salida:

Ana
25

static

La palabra clave static indica que algo pertenece a la clase, en lugar de pertenecer a cada objeto individual.

Esta diferencia es muy importante.

Hasta ahora hemos trabajado con atributos que pertenecen a cada objeto.

Por ejemplo:

public class Persona {

String nombre;
int edad;

}

Si creamos dos personas:

Persona persona1 = new Persona();
Persona persona2 = new Persona();

persona1.nombre = "Ana";
persona2.nombre = "Luis";

Cada objeto tiene su propio nombre.

persona1 → Ana
persona2 → Luis

Esto ocurre porque nombre no es static.


Atributos static

Imaginemos ahora que queremos almacenar un dato que sea común para todas las personas.

Por ejemplo, queremos saber cuántas personas hemos creado.

Podemos utilizar un atributo static:

public class Persona {

String nombre;

static int cantidadPersonas = 0;

}

Este atributo pertenece a la clase Persona, no a cada persona individual.

Podemos incrementarlo desde el constructor:

public Persona(String nombre) {

this.nombre = nombre;

cantidadPersonas++;

}

Ahora podemos crear varios objetos:

Persona persona1 = new Persona("Ana");
Persona persona2 = new Persona("Luis");
Persona persona3 = new Persona("María");

La variable cantidadPersonas tendrá el valor:

3

Esto ocurre porque los tres objetos comparten el mismo atributo static. Es decir, cantidadPersona no tiene un valor diferente para cada objeto de tipo Persona que se cree, sino que existe un único atributo cantidadPersona perteneciente a la clase en sí.


Acceder a un atributo static

Como un atributo static pertenece a la clase, podemos acceder a él utilizando el nombre de la clase en lugar del nombre del objeto creado:

System.out.println(Persona.cantidadPersonas);

Salida:

3

No necesitamos crear un objeto adicional para acceder a este atributo, sino que accedemos a él directamente con el nombre de la clase Persona..

Incorrecto:

Persona persona1 = new Persona("Mario", 22);
System.out.println(persona1.cantidadPersonas);

Correcto:

System.out.println(Persona.cantidadPersonas);

Métodos static

Los métodos también pueden ser static.

Un método static pertenece a la clase y puede utilizarse sin crear un objeto.

Por ejemplo:

public class Calculadora {

public static int sumar(int a, int b) {

return a + b;

}

}

Podemos utilizar el método directamente desde la clase:

int resultado = Calculadora.sumar(5, 3);

System.out.println(resultado);

Salida:

8

No hemos necesitado hacer:

Calculadora calculadora = new Calculadora();

Esto es posible porque sumar() es static.


main y static

Ahora podemos entender mejor por qué el método main tiene la palabra static.

Cuando escribimos:

public static void main(String[] args) {

}

el método main es static.

Esto permite que Java pueda ejecutar el programa sin tener que crear previamente un objeto de la clase que contiene main.

Podemos recordar:

public

Puede ser accesible desde fuera

static

Pertenece a la clase

void

No devuelve ningún valor

main

Punto de entrada del programa

static y atributos de los objetos

Hay que tener cuidado al trabajar con static.

Un método static pertenece a la clase, por lo que no puede acceder directamente a los atributos normales de un objeto.

Por ejemplo:

public class Persona {

String nombre;

public static void mostrarNombre() {

System.out.println(nombre);

}

}

Esto no funcionará correctamente.

¿Por qué?

Porque nombre pertenece a cada objeto.

En cambio:

mostrarNombre()

pertenece a la clase.

Java no sabe de qué objeto debería obtener el nombre.

Un método static sí puede acceder directamente a atributos que también sean static.

public class Persona {

static int cantidadPersonas = 0;

public static void mostrarCantidad() {

System.out.println(cantidadPersonas);

}

}

Ahora ambos pertenecen a la clase Persona.


Clases y ficheros

Una duda muy habitual cuando empezamos a trabajar con clases es:

¿Las clases van en el mismo fichero que el main o en un fichero separado?

La respuesta depende de cómo estemos organizando el programa, pero en proyectos reales lo habitual es mantener las clases en ficheros separados.

Por ejemplo:

Proyecto

├── Main.java
├── Persona.java
├── Producto.java
└── Coche.java

Cada clase tiene su propio fichero.

Esto permite mantener el código organizado y facilita su mantenimiento.


El fichero Main

Normalmente tenemos una clase que contiene el método:

public static void main(String[] args)

Por ejemplo:

public class Main {

public static void main(String[] args) {

Persona persona = new Persona("Ana", 25);

persona.mostrarInformacion();

}

}

El main es el punto de entrada del programa.

Desde él podemos crear objetos y utilizar sus métodos.


El fichero de la clase

La clase Persona podría estar en un fichero llamado:

Persona.java

Y contener:

public class Persona {

private String nombre;
private int edad;

public Persona(String nombre, int edad) {

this.nombre = nombre;
this.edad = edad;

}

public void mostrarInformacion() {

System.out.println("Nombre: " + nombre);
System.out.println("Edad: " + edad);

}

}

Después podemos utilizar esta clase desde Main.


Llamar a una clase desde Main

Si las dos clases están en el mismo paquete, normalmente podemos utilizar directamente la clase.

public class Main {

public static void main(String[] args) {

Persona persona = new Persona("Ana", 25);

persona.mostrarInformacion();

}

}

Si la clase se encuentra en otro paquete, debemos importarla.

Por ejemplo:

import personas.Persona;

Después podremos utilizarla:

Persona persona = new Persona("Ana", 25);

Ejemplo completo

Vamos a juntar todos los conceptos vistos hasta ahora.

Tenemos una clase Persona con:

  • Dos atributos.
  • Un constructor.
  • this.
  • Métodos.
  • Un atributo static.
public class Persona {

private String nombre;
private int edad;

static int cantidadPersonas = 0;

public Persona(String nombre, int edad) {

this.nombre = nombre;
this.edad = edad;

cantidadPersonas++;

}

public String obtenerNombre() {

return nombre;

}

public int obtenerEdad() {

return edad;

}

public void mostrarInformacion() {

System.out.println("Nombre: " + nombre);
System.out.println("Edad: " + edad);

}

}

Ahora podemos utilizar la clase desde Main:

public class Main {

public static void main(String[] args) {

Persona persona1 = new Persona("Ana", 30);
Persona persona2 = new Persona("Luis", 25);

persona1.mostrarInformacion();

System.out.println();

persona2.mostrarInformacion();

System.out.println();

System.out.println("Personas creadas: " + Persona.cantidadPersonas);

}

}

Salida:

Nombre: Ana
Edad: 30

Nombre: Luis
Edad: 25

Personas creadas: 2

Errores frecuentes

Confundir una clase con un objeto

La clase:

Persona

es la plantilla.

El objeto:

Persona persona1 = new Persona("Ana", 25);

es una instancia concreta de esa clase.

Podemos crear muchos objetos a partir de una misma clase.


Olvidar new

Incorrecto:

Persona persona1;

Aquí solamente hemos declarado una variable.

Todavía no hemos creado ningún objeto.

Para crear el objeto:

Persona persona1 = new Persona("Ana", 25);

Confundir el constructor con un método

El constructor:

public Persona(String nombre, int edad) {

}

tiene el mismo nombre que la clase.

Además, no tiene tipo de retorno.

No debemos escribir:

public void Persona(String nombre, int edad) {

}

Eso sería un método, no un constructor.


Olvidar this

Si utilizamos el mismo nombre para los atributos y los parámetros:

public Persona(String nombre, int edad) {

this.nombre = nombre;
this.edad = edad;

}

necesitamos utilizar this para distinguir el atributo del parámetro.


Utilizar static sin necesidad

No debemos convertir todos los atributos y métodos en static.

Utilizaremos static cuando el dato o comportamiento pertenezca a la clase en general, y no a un objeto concreto.

Por ejemplo:

nombre
edad

pertenecen a cada persona.

Mientras que:

cantidadPersonas

puede ser un dato compartido por todas las personas.