Saltearse al contenido

Clases definidas por el usuario

Como mencionamos en el tema anterior, trabajar con objetos es imprescindible para poder programar en Java. Si bien es cierto que Java nos provee de una serie de clases que podemos utilizar, también es cierto que podemos crear nuestras propias clases.

En esencia, estaremos definiendo un nuevo tipo de dato, que podremos utilizar en nuestro programa. Además, como sabemos, los objetos son instancias de una clase, por lo cual típicamente suele referirse a las clases como plantillas, prototipos o moldes de los objetos.

El hecho de definir una clase implica que necesitamos representar un concepto, un objeto, una entidad, etc. que sea relevante para nuestro programa. Por ejemplo, si estamos desarrollando un sistema de gestión de una biblioteca, es probable que necesitemos representar a los libros, a los usuarios, a los préstamos, etc. En este caso, cada uno de estos conceptos podría ser representado por una clase.

Para definir una clase en Java, utilizamos la palabra reservada class, seguida del nombre de la clase y de un bloque de código entre llaves.

1
class <ClassName> {
2
// Cuerpo de la clase
3
}

Por ejemplo, si quisiéramos definir una clase Car, podríamos hacerlo de la siguiente manera:

1
class Car {
2
// Cuerpo de la clase
3
}

Claramente, esto no es suficiente para representar a un auto, pero es la mínima expresión que necesitamos para definir una clase. En este caso, la clase Car no tiene ningún atributo ni ningún método, por lo cual no es muy útil. Sin embargo, es una clase válida y podría ser utilizada en un programa.

1
Car car = new Car();

En este caso, estamos creando una instancia de la clase Car y asignándola a la variable car. Es decir, creamos un usuario en particular, que podemos utilizar en nuestro programa.

Así diferenciamos semánticamente entre las clases y los objetos, pero siempre podemos darles nombres más descriptivos a nuestras instancias, por ejemplo:

1
Car deportivoRojo = new Car();

Atributos

Para que una clase sea útil, es necesario que tenga atributos y métodos que nos permitan representar a la entidad que estamos modelando. En el ejemplo planteado, un auto podría tener un color, una marca, un modelo, el kilometraje, entre otros atributos.

Para definir un atributo, debemos indicar el tipo de dato que va a almacenar, seguido del nombre del atributo. Es decir, un atributo es una variable que pertenece a la clase. Por ejemplo, si quisiéramos agregar un atributo color a la clase Car, podríamos hacerlo de la siguiente manera:

1
class Car {
2
String color;
3
}

De igual manera, podemos definir otros atributos que necesitemos.

1
class Car {
2
String color; // Color
3
String brand; // Marca
4
String model; // Modelo
5
int mileage; // Kilometraje
6
boolean isOn; // Está encendido
7
}

Ahora, podemos crear diferentes instancias de la clase Car y asignarles valores a sus atributos.

1
Car deportivoRojo = new Car();
2
deportivoRojo.color = "rojo";
3
deportivoRojo.brand = "Ferrari";
4
deportivoRojo.model = "F40";
5
deportivoRojo.mileage = 10000;
6
deportivoRojo.isOn = false;
7
8
Car familiarAzul = new Car();
9
familiarAzul.color = "azul";
10
familiarAzul.brand = "Ford";
11
familiarAzul.model = "Focus";
12
familiarAzul.mileage = 200000;
13
familiarAzul.isOn = true;

Al igual, que con cualquier variable, podemos asignarle valores a los atributos de un objeto y utilizarlos en nuestro programa.

Por otro lado, es importante tener en cuenta que los atributos de una clase son variables de instancia. Es decir, cada instancia de la clase tiene sus propios atributos, que pueden tener valores diferentes a los de otras instancias de la misma clase, como vimos en el ejemplo anterior.

Así también, podemos asignar valores a los atributos al momento de definirlos, utilizando el operador de asignación =. Estos valores serán los valores por defecto de los atributos de cada instancia de la clase.

1
class Car {
2
String color; // Color
3
String brand; // Marca
4
String model; // Modelo
5
int mileage = 0; // Kilometraje
6
boolean isOn = false; // Está encendido
7
}

De esta manera, cada vez que creemos una instancia de la clase Car, los atributos mileage e isOn tendrán los valores por defecto que definimos en la clase, lo cual puede ser útil en algunos casos.

1
Car deportivoVerde = new Car();
2
System.out.println(deportivoVerde.mileage); // 0
3
System.out.println(deportivoVerde.isOn); // false

En este punto, hemos dotado a nuestra clase con características (o atributos) que nos permiten identificar a un auto. Si bien podrían tener más atributos, nos quedaremos con estos tres por el momento.

Sin embargo, que una clase permita almacenar datos no es suficiente. También necesitamos que tenga comportamiento. Es decir, que nos permita realizar acciones sobre los datos que almacena. Para ello, utilizaremos los métodos.

Métodos

Los métodos son bloques de código que nos permiten realizar acciones sobre los datos que almacena una clase. En esencia, es comportamiento asociado al objeto que estamos modelando.

Para definir un método debemos tener en cuenta las siguientes consideraciones:

  • Un método debe tener un nombre, que nos permita identificarlo. Generalmente, se utiliza un verbo en infinitivo para nombrar a los métodos. Por ejemplo, si quisiéramos definir un método que nos permita encender el auto, podríamos llamarlo turnOn o encender.
  • Un método puede recibir parámetros o no, los cuales nos permiten enviarle información al método para que pueda realizar el comportamiento que necesitamos.
  • Un método puede devolver un valor o no, dependiendo de si necesitamos que nos devuelva información o no.

En Java, para definir un método, debemos indicar el tipo de dato que va a devolver (o void si no devuelve nada), seguido del nombre del método y entre paréntesis, los parámetros que recibe.

1
<tipo que retorna> <nombre método>([<tipo de parámetro> <nombre parámetro>, ...]) {
2
<cuerpo del método>
3
[return <valor de retorno>];
4
}

Nótese que los parámetros son opcionales, por lo cual los paréntesis pueden estar vacíos. Sin embargo, los paréntesis son obligatorios, ya que indican que estamos definiendo un método.

Por otro lado, un método puede tener una sentencia return o no. En caso de tenerla, esto indica que el método devuelve un valor y el tipo de dato del valor de retorno debe coincidir con el tipo de dato de retorno del método. En caso de no tenerla, el tipo de dato de retorno del método debe ser void.

Es importante resaltar que, al igual que con las variables, debemos declarar un método dentro de una clase.

Valor de retorno

Los métodos pueden devolver un valor o no, este valor es el resultado de la ejecución del método y denominado valor de retorno. Este valor puede ser de cualquier tipo de dato, incluso de un tipo de dato definido por el usuario (es decir, una clase).

Por ejemplo, si quisiéramos conocer la marca de un auto podríamos definir un método que nos devuelva dicho valor. En este caso, el tipo de dato de retorno del método sería String, ya que la marca es un String. Además, en este caso, el método no recibe ningún parámetro, ya que no necesitamos enviarle información adicional para que nos devuelva la marca del auto.

1
String getBrand() {
2
// Cuerpo del método
3
return // Valor de retorno
4
}

Por otro lado, veremos que en ocasiones no necesitamos, o no deberíamos, devolver ningún valor.

Por ejemplo, si quisiéramos representar el comportamiento para encender el auto, podríamos hacerlo definiendo el método turnOn para la clase Car, podríamos hacerlo de la siguiente manera:

1
void turnOn() {
2
// Cuerpo del método
3
}

En este caso, el método turnOn no recibe ningún parámetro y no devuelve ningún valor.

En ocasiones veremos que un método puede realizar una acción, pero no devuelve ningún valor porque no es necesario. Por ejemplo, porque se modifica el estado de un objeto, o porque se muestra un mensaje por pantalla, etc. En estos casos, el tipo de dato de retorno del método es void, lo cual indica la carencia de un valor de retorno.

Referencia this

Entonces, si quisiéramos definir el método turnOn para la clase Car, deberíamos modificar el atributo isOn a false para indicar que el auto está encendido. Y para el método getBrand, deberíamos devolver el valor del atributo brand del auto.

Podríamos pensar que las siguientes definiciones para estos métodos son correctas:

1
class Car {
2
String color; // Color
3
String brand; // Marca
4
String model; // Modelo
5
int mileage; // Kilometraje
6
boolean isOn; // Está encedido
7
8
void turnOn() {
9
isOn = true;
10
}
11
12
String getBrand() {
13
return brand;
14
}
15
}

No obstante, ¿cómo sabemos que estamos cambiando el valor del atributo isOn del auto correcto? Si tenemos dos instancias de la clase Car, deportivoRojo y familiarAzul ¿a cuál de ellas le estamos cambiando el valor del atributo isOn?

Para indicar que queremos modificar un atributo de una instancia en particular, utilizamos la palabra reservada this. Esta nos permite referenciar al objeto que está ejecutando el método.

1
class Car {
2
String color; // Color
3
String brand; // Marca
4
String model; // Modelo
5
int mileage; // Kilometraje
6
boolean isOn; // Está encedido
7
8
void turnOn() {
9
this.isOn = true;
10
}
11
12
String getBrand() {
13
return this.brand;
14
}
15
}

Entonces, dependiendo de la instancia que invoque al método turnOn, se modificará el atributo isOn de esa instancia en particular.

1
Car deportivoRojo = new Car();
5 collapsed lines
2
deportivoRojo.color = "rojo";
3
deportivoRojo.brand = "Ferrari";
4
deportivoRojo.model = "F40";
5
deportivoRojo.mileage = 10000;
6
deportivoRojo.isOn = false;
7
8
Car familiarAzul = new Car();
5 collapsed lines
9
familiarAzul.color = "azul";
10
familiarAzul.brand = "Ford";
11
familiarAzul.model = "Focus";
12
familiarAzul.mileage = 200000;
13
familiarAzul.isOn = true;
14
15
System.out.println(deportivoRojo.isOn); // false
16
System.out.println(familiarAzul.isOn); // true
17
18
deportivoRojo.turnOn();
19
20
System.out.println(deportivoRojo.isOn); // true
21
System.out.println(familiarAzul.isOn); // true

De igual manera, ocurrirá con el método getBrand, que devolverá el valor del atributo brand de la instancia que invoque al método.

1
System.out.println(deportivoRojo.getBrand()); // Ferrari
2
System.out.println(familiarAzul.getBrand()); // Ford

Es importante tener en cuenta que la referencia this solo puede ser utilizada dentro de una clase. Fuera de ella carece de sentido, ya que no hay un objeto que la invoque.

Parámetros

Por otro lado, al definir un método veremos la necesidad de enviar información adicional a la que un objeto almacena. Por ejemplo, si quisiéramos cambiar el color de un auto, necesitaríamos enviarle al método el nuevo color que queremos asignarle.

Para ello, podemos definir un parámetro en el método, que nos permita recibir información adicional. Para definir un parámetro, debemos indicar el tipo de dato que va a recibir dicho parámetro, seguido del nombre del parámetro.

En este caso, el parámetro sería el nuevo color que queremos asignarle al auto, la cual debería ser del mismo tipo de dato que el atributo que queremos modificar (en este caso, color es de tipo String).

1
class Car {
2
String color; // Color
3
String brand; // Marca
4
String model; // Modelo
5
int mileage; // Kilometraje
6
boolean isOn; // Está encedido
7
8
void turnOn() {
9
this.isOn = true;
10
}
11
12
void paint(String newColor) {
13
this.color = newColor;
14
}
15
}

Por supuesto, podemos definir tantos parámetros como necesitemos para nuestros métodos.

Punto de entrada de un programa

Al ser Java un lenguaje orientado a objetos, es importante tener en cuenta que todo el código que escribamos debe estar contenido dentro de una clase. Lo que es más, hemos estado definiendo clases desde el primer momento, ya que cada vez que escribimos un programa, estamos definiendo una clase.

Si recordamos, para ejecutar un programa en Java, definimos la clase Main. La misma contiene el método main, que es el punto de entrada de nuestro programa. Es decir, para ejecutar un programa en Java, debemos definir la clase que coordina la ejecución del mismo a través de un comportamiento, que es el método main.

1
class Main {
2
public static void main(String[] args) {
3
// Cuerpo del método
4
}
5
}

Este método, a diferencia de otros, siempre debe tener este nombre y esta firma. Es decir, siempre debe llamarse main y recibir un parámetro de tipo String[] (un array de String). Además, siempre debe ser public y static (más adelante hablaremos de estos modificadores).