Ricardo Pérez López
IES Doñana, curso 2024/2025
new
La operación new
permite
instanciar un objeto a partir de una clase.
Hay que indicar el nombre de la clase y pasarle al constructor los argumentos que necesite, entre paréntesis y separados por comas. Los paréntesis son obligatorios aunque no haya argumentos.
Por ejemplo, si tenemos una clase Triángulo
cuyo
constructor espera dos argumentos (ancho y alto),
podemos crear una instancia de esa clase de la siguiente forma:
getClass
El método getClass()
devuelve la clase de la que es instancia el objeto sobre el que se
ejecuta.
Lo que devuelve es una instancia de la clase java.lang.Class
.
Para obtener una cadena con el nombre de la clase, se puede usar
el método getSimpleName()
definido en la clase Class
:
instanceof
El operador instanceof
permite comprobar si un objeto es instancia de una determinada
clase.
Por ejemplo:
Sólo se puede aplicar a referencias, no a valores primitivos:
Los objetos son accesibles a través de referencias.
Las referencias se pueden almacenar en variables de tipo referencia.
Por ejemplo, String
es una
clase, y por tanto es un tipo referencia. Al hacer la siguiente
declaración:
estamos declarando s
como una variable que puede
contener una referencia a un valor de tipo String
.
null
El tipo null
sólo tiene
un valor: la referencia nula, representada por el literal null
.
El tipo null
es
compatible con cualquier tipo referencia.
Por tanto, una variable de tipo referencia siempre puede contener la referencia nula.
En la declaración anterior:
la variable s
puede contener una referencia a un objeto
de la clase String
, o bien
puede contener la referencia nula null
.
La referencia nula sirve para indicar que la variable no apunta a ningún objeto.
Al intentar invocar a un método desde una referencia nula, se
lanza una excepción NullPointerException
:
El operador ==
aplicado a dos objetos (valores de
tipo referencia) devuelve true
si ambos son
el mismo objeto.
Es decir: el operador ==
compara la
identidad de los objetos para preguntarse si son
idénticos.
Equivale al operador is
de
Python.
Para usar un mecanismo más sofisticado que realmente pregunte si
dos objetos son iguales, hay que usar el método equals
.
equals
El método equals
compara dos
objetos para comprobar si son iguales.
Debería usarse siempre en sustitución del operador
==
, que sólo comprueba si son idénticos.
Equivale al __eq__
de
Python, pero en Java hay que llamarlo explícitamente (no se llama
implícitamente al usar ==
).
La implementación predeterminada del método equals
se hereda de la clase Object
(que ya
sabemos que es la clase raíz de la jerarquía de clases en Java, por lo
que toda clase acaba siendo subclase, directa o indirecta, de Object
).
En dicha implementación predeterminada, equals
equivale a ==
:
Por ello, es importante sobreescribir dicho método al crear
nuevas clases, ya que, de lo contrario, se comportaría igual que
==
.
compareTo
Un método parecido es compareTo
, que compara dos objetos de
forma que la expresión a.compareTo(b)
devuelve un entero:
Menor que cero si a < b
.
0
si
a == b
.
Mayor que cero si a > b
.
hashCode
El método hashCode
equivale
al __hash__
de
Python.
Como en Python, devuelve un número entero (en este caso, de 32
bits) asociado a cada objeto, de forma que si dos objetos son iguales,
deben tener el mismo valor de hashCode
.
Por eso (al igual que ocurre en Python), el método hashCode
debe coordinarse con el método
equals
.
A diferencia de lo que ocurre en Python, en Java todos los objetos son hashables. De hecho, no existe el concepto de hashable en Java, ya que no tiene sentido.
Este método se usa para acelerar la velocidad de almacenamiento y
recuperación de objetos en determinadas colecciones como HashMap
, HashSet
o Hashtable
.
La implementación predeterminada de hashCode
se hereda de la clase Object
, y
devuelve un valor que depende de la posición de memoria donde está
almacenado el objeto.
Al crear nuevas clases, es importante sobreescribir dicho método
para que esté en consonancia con el método equals
y garantizar que siempre se cumple
que:
Si x.equals(
y)
, entonces x.hashCode()
==
y.hashCode()
.
Los objetos en Java no se destruyen explícitamente, sino que se marcan para ser eliminados cuando no hay ninguna referencia apuntándole:
La próxima vez que se active el recolector de basura, el objeto se eliminará de la memoria.
En Java, las cadenas son objetos.
Por tanto, son valores referencia, instancias de una determinada clase.
Existen dos tipos de cadenas:
Inmutables: instancias de la clase String
.
Mutables: instancias de las clases StringBuffer
o
StringBuilder
.
Las cadenas inmutables son objetos de la clase String
.
Las cadenas literales (secuencias de caracteres encerradas entre
dobles comillas "
) son
instancias de la clase String
:
Otra forma de crear un objeto de la clase String
es
instanciando dicha clase y pasándole otra cadena al constructor. De esta
forma, se creará un nuevo objeto cadena con los mismos caracteres que la
otra cadena:
Si se usa varias veces el mismo literal cadena, el JRE intenta aprovechar el objeto ya creado y no crea uno nuevo:
Las cadenas creadas mediante instanciación, siempre son objetos distintos:
Pregunta: ¿cuántos objetos cadena se crean en cada caso?
Los objetos de la clase String
disponen
de métodos que permiten realizar operaciones con cadenas.
Muchos de ellos devuelven una nueva cadena a partir de la original tras una determinada transformación.
Algunos métodos interesantes son:
length
indexOf
lastIndexOf
charAt
repeat
replace
startsWith
endsWith
substring
toUpperCase
toLowerCase
La clase String
también
dispone de métodos estáticos.
El más interesante es valueOf
, que devuelve la representación
en forma de cadena de su argumento:
No olvidemos que, en Java, los caracteres y las cadenas son tipos distintos:
Un carácter es un valor primitivo de tipo char
y sus
literales se representan entre comillas simples ('a'
).
Una cadena es un valor referencia de tipo String
y sus
literales se representan entre comillas dobles ("a"
).
Un objeto de la clase String
no puede
modificarse una vez creado.
Es exactamente lo que ocurre con las cadenas en Python.
En Java existen cadenas mutables que sí permiten su modificación después de haberse creado.
Para ello, proporciona dos clases llamadas StringBuffer
y
StringBuilder
,
cuyas instancias son cadenas mutables.
Las dos funcionan prácticamente de la misma forma, con la única
diferencia de que los objetos StringBuffer
permiten sincronización entre hilos mientras que los StringBuilder
no.
Cuando se está ejecutando un único hilo, es preferible usar
objetos StringBuilder
ya
que son más eficientes.
Se puede crear un objeto StringBuilder
vacío o a partir de una cadena:
StringTokenizer
La clase StringTokenizer
permite romper una cadena en tokens.
El método de tokenización consiste en buscar los elementos separados por delimitadores, que son los caracteres que separan los tokens.
Esos delimitadores pueden especificarse en el momento de crear el tokenizador o bien token a token.
Por ejemplo:
StringTokenizer st = new StringTokenizer("esto es una prueba");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
produce la siguiente salida:
esto
es
una
prueba
La clase StringTokenizer
se mantiene por compatibilidad pero su uso no se recomienda en código
nuevo.
En su lugar, se recomienda usar el método split
de la clase String
o el
paquete java.util.regex
.
Por ejemplo:
Los métodos definidos en la clase String
se pueden
consultar en la API de Java:
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/String.html
String
La conversión de un objeto a String
se realiza
llamando al método toString
del
objeto.
Todo objeto, sea de la clase que sea, tiene un método toString
heredado de la clase Object
y
posiblemente sobreescribiéndo éste.
Si es un valor primitivo, primero se convierte a instancia de su clase wrapper correspondiente.
La operación de concatenación de cadenas se realiza con el
operador +
:
También existe el método concat
, que hace lo mismo:
En las cadenas, las comparaciones se pueden realizar:
Con el operador ==
:
No es conveniente, ya que comprueba si los dos objetos son el mismo (y eso sólo está garantizado si las dos cadenas son literales).
Con el método equals
:
Comprueba si las dos cadenas tienen los mismos caracteres.
Con el método compareTo
:
También se puede usar el método estático equals
de la clase Objects
del paquete java.util
(cuidado: es Objects
en
plural, no Object
):
Es como hacer:
pero el equals
de Objects
tiene la ventaja de que no
provoca un error NullPointerException
si str1
es null
.
String
Los literales cadena se almacenan en un pool de cadenas y se reutilizan siempre que se puede.
Los objetos String
van
asociados a un literal cadena almacenado en el pool.
Se puede acceder a ese literal del objeto cadena usando el método
intern
:
Las clases envolventes (también llamadas clases wrapper) son clases cuyas instancias representan valores primitivos almacenados dentro de valores referencia.
Esos valores referencia envuelven al valor primitivo dentro de un objeto.
Se utilizan en contextos en los que se necesita manipular un dato primitivo como si fuera un objeto, de una forma sencilla y transparente.
Existe una clase wrapper para cada tipo primitivo:
Clase wrapper | Tipo primitivo |
---|---|
java.lang.Boolean |
boolean |
java.lang.Byte |
byte |
java.lang.Short |
short |
java.lang.Character |
char |
java.lang.Integer |
int |
java.lang.Long |
long |
java.lang.Float |
float |
java.lang.Double |
double |
Los objetos de estas clases disponen de métodos para acceder a los valores envueltos dentro del objeto.
Por ejemplo:
A partir de JDK 9, los constructores de las clases wrapper han quedado obsoletos.
Actualmente, se recomienda usar uno de los métodos estáticos
valueOf
para obtener un objeto
wrapper.
El método es un miembro estático de todas las clases wrappers y todas las clases numéricas admiten formas que convierten un valor numérico o una cadena en un objeto.
Por ejemplo:
El boxing es el proceso de envolver un valor primitivo en una referencia a una instancia de su correspondiente clase wrapper. Por ejemplo:
El unboxing es el proceso de extraer un valor primitivo a partir de una instancia de su correspondiente clase wrapper. Por ejemplo:
A partir de JDK 5, este proceso se puede llevar a cabo automáticamente mediante el autoboxing y el autounboxing.
El autoboxing es el mecanismo que convierte automáticamente un valor primitivo en una referencia a una instancia de su correspondiente clase wrapper. Por ejemplo:
El autounboxing es el mecanismo que convierte automáticamente una instancia de una clase wrapper en su valor primitivo equivalente. Por ejemplo:
Number
La clase java.lang.Number
en Java es una clase abstracta que representa la clase base (o
superclase) de todas las clases que representan valores numéricos
convertibles a valores primitivos de tipo byte
, double
, float
, int
, long
y short
.
Esta clase es la superclase de todas las clases wrapper
que representan valores numéricos: Byte
, Double
, Float
, Integer
, Long
y Short
.
También es la superclase de las clases java.math.BigInteger
y java.math.BigDecimal
cuyas instancias representan, respectivamente, números enteros y reales
de precisión arbitraria.
La clase abstracta Number
declara (o define) los
siguientes métodos:
byte byteValue() |
abstract double doubleValue() |
abstract float floatValue() |
abstract int intValue() |
abstract long longValue() |
short shortValue() |
Los métodos byteValue
y
shortValue
son concretos porque
devuelven el valor de intValue()
convertido, respectivamente, a byte
o a short
a través de
un casting (byte)
o (short)
.
Los métodos abstractos se implementan luego en sus subclases.
Ejemplo de uso:
En Java, un array es un dato mutable compuesto por elementos (también llamados componentes) a los que se accede mediante indexación, es decir, indicando la posición donde se encuentra almacenado el elemento deseado dentro del array.
Se parece a las listas de Python, con las siguientes diferencias:
Cada array tiene una longitud fija (no puede crecer o encogerse de tamaño dinámicamente).
Todos los elementos de un array deben ser del mismo tipo, el cual debe indicarse en la declaración del array.
Los arrays en Java pueden contener valores primitivos o referencias a objetos.
Los arrays de Java son objetos y, por tanto, son valores referencia.
Los arrays se declaran indicando el tipo del elemento
que contienen, seguido de []
.
Por ejemplo, para declarar un array de enteros, se puede hacer:
Ahora mismo, x
es una referencia a un objeto
array que puede contener elementos de tipo int
. Como la
variable x
aún no ha sido inicializada, el valor que
contiene es la referencia nula (null
):
Por tanto, x
puede hacer referencia a un
array de enteros, pero actualmente no hace referencia a
ninguno.
A esa variable le podemos asignar una referencia a un objeto array del tipo adecuado.
Para ello, se puede crear un objeto array usando el
operador new
e indicando
el tipo de los elementos y la longitud del array (entre
corchetes):
A partir de este momento, la variable x
contiene una
referencia a un objeto array de cinco elementos de tipo int
que, ahora
mismo, tienen todos el valor 0
.
Como se puede observar, los elementos de un array
siempre se inicializan a un valor por defecto cuando se
crea el array (0
en enteros,
0.0
en
reales, false
en
booleanos, '\000'
en
caracteres y null
en valores
referencia).
También se pueden inicializar los elementos de un array
en el momento en que se crea con new
.
En ese caso:
Se indican los valores de los elementos del array entre llaves.
No se indica la longitud del array, ya que se deduce a partir de la lista de valores iniciales.
Por ejemplo:
Para acceder a un elemento del array se usa el operador de indexación (los corchetes):
Los elementos se indexan de 0 a n - 1, siendo n la longitud del array.
Si se intenta acceder a un elemento fuera de esos límites, se
levanta una excepción java.lang.ArrayIndexOutOfBoundsException
:
Para conocer la longitud de un array, se puede consultar
el atributo length
:
Ese valor es constante y no se puede cambiar:
Para cambiar un elemento del array por otro, se puede usar la indexación combinada con la asignación:
El compilador comprueba que el valor a asignar es del tipo correcto, e impide la operación si se ve obligado a hacer un narrowing para hacer que el tipo del valor sea compatible con el tipo del elemento:
Los elementos de un array también pueden ser valores referencia.
En ese caso, sus elementos serán objetos de una determinada clase.
Inicialmente, los elementos referencia del array toman
el valor null
.
Por ejemplo:
En cada elemento de cadenas
podremos meter una
instancia de la clase String
:
También podemos inicializar el array con objetos:
Hemos dicho que los elementos de un array de tipos referencia deben ser objetos de una determinada clase, que es la clase indicada al declarar el array.
Pero por el principio de sustitución, esos elementos también pueden ser instancias de una subclase de esa clase.
Por ejemplo, si tenemos la clase Figura
y una
subclase suya llamada Triangulo
:
En cada elemento de figuras
podremos meter una
instancia de la clase Figura
o de cualquier subclase
suya:
Si declaramos un array de tipo Object[]
,
estamos diciendo que sus elementos pueden ser de cualquier tipo
referencia, lo que tiene ventajas e inconvenientes:
Ventaja: los elementos del array podrán ser de cualquier tipo, incluyendo tipos primitivos (recordemos el boxing/unboxing).
Inconveniente: no podremos aprovechar el comprobador de tipos del compilador para determinar si los tipos son los adecuados, por lo que tendremos que hacerlo a mano en tiempo de ejecución.
Entre los tipos de arrays se define una relación de subtipado directo (<_1) similar a la que hemos visto hasta ahora.
Resumiendo, las reglas que definen esa relación son las siguientes:
Si S y T son tipos referencia, entonces S[]
<_1 T[]
si y sólo si S <_1 T.
Debido a esto, se dice que los arrays de Java son covariantes con los tipos referencia (hablaremos más sobre este tema cuando estudiemos los tipos genéricos).
Object[]
<_1
Object
.
Si P es un tipo primitivo,
entonces P[]
<_1 Object
.
java.util.Arrays
La clase java.util.Arrays
contiene varios métodos estáticos para manipular arrays,
incluyendo ordenación y búsqueda.
También contiene un método factoría estático que permite ver a los arrays como listas, lo que será de interés cuando veamos las listas en Java.
Los métodos de esta clase lanzan todos una excepción NullPointerException
cuando el array especificado es una referencia nula.
Su documentación se encuentra en el API de Java:
https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/util/Arrays.html
Para hacer una copia de un array, se pueden usar varios métodos:
clone
de la clase Object
.
System.arraycopy
.
Arrays.copyOf
.
Arrays.copyOfRange
.
Todos los métodos hacen copias superficiales (shallow copys) del array.
Para hacer una copia profunda (deep copy) es necesario escribir un trozo de código que lo haga.
clone
La clase Object
proporciona el método clone
.
Como los arrays en Java también son objetos de la clase
Object
, se
puede usar este método para copiar un array.
Este método copia todos los elementos del array, así que no sirve si lo que se quiere es hacer una copia parcial del array, es decir, si se quieren copiar sólo algunos elementos (un subarray).
Por ejemplo:
System.arraycopy
El método arraycopy
de la
clase java.lang.System
es la mejor forma de hacer una copia parcial del
array.
Ofrece una forma sencilla de especificar el número total de elementos a copiar así como los índices de origen y destino.
Su signatura es:
Por ejemplo, el siguiente código:
copiará 5 elementos del array origen
en el
array destino
, empezando por el índice 3 de
origen
y desde el índice 2 de
destino
.
Arrays.copyOf
Se puede usar el método copyOf
de la clase java.util.Arrays
cuando se desean copiar los primeros elementos del
array.
También se puede usar cuando se desea extender el array creando un nuevo array con los mismos elementos del array original pero con más elementos al final, los cuales se rellenarán con los valores iniciales por defecto del tipo de los elementos del array.
El método copyOf
está
sobrecargado para poder copiar arrays de cualquier tipo
primitivo.
Además, es un método genérico (ya veremos en su momento lo que eso significa), lo que le permite copiar arrays de cualquier tipo referencia.
La signatura de los métodos copyOf
sigue el siguiente esquema:
donde T
es el tipo de los elementos del array
original
.
El método devuelve un nuevo array a partir del original,
con tantos elementos como se haya indicado en
newLength
.
Cuando newLength
< original.length
,
el array nuevo tendrá sólo los primeros newLength
elementos del original.
Cuando newLength
> original.length
,
el array nuevo tendrá todos los elementos del original, y se
rellenará por el final con tantos valores como sea necesario para que el
nuevo array tenga newLength
elementos.
Esos valores serán los valores iniciales por defecto del tipo
T
.
Por ejemplo, si tenemos:
Creamos un nuevo array con menos elementos:
Creamos otro array con más elementos:
jshell> Arrays.copyOf(s, 7)
$3 ==> String[7] { "hola", "pepe", "juan", "maría", "antonio", null, null }
Los nuevos elementos toman el valor null
, que es el
valor por defecto para los tipos referencia.
Para comprobar si dos arrays a1
y
a2
son iguales, podríamos usar:
a1 == a2
, pero no resulta muy adecuado porque ya
sabemos que lo que comprueba es si son
idénticos.
a1.equals(a2)
,
pero tampoco resulta adecuado porque la implementación del método equals
en los arrays es la que
se hereda de la clase Object
, la cual
es idéntica a a1 == a2
.
Arrays.equals
La mejor opción para comparar dos arrays es usar el
método equals
de la clase java.util.Arrays
.
El método Arrays.equals
está sobrecargado para poder comparar arrays de cualquier tipo
primitivo.
Además, es un método genérico (ya veremos en su momento lo que eso significa), lo que le permite comparar arrays de cualquier tipo referencia.
Las signaturas de los métodos equals
siguen el siguiente esquema:
static boolean equals(T[] a, T[] a2)
static boolean equals(T[] a, int aFromIndex, int aToIndex, T[] b,
int bFromIndex, int bToIndex)
donde T
es el tipo de los elementos del array
original
.
Ejemplo de uso:
jshell> String[] s = new String[] { "hola", "pepe", "juan" };
s ==> String[3] { "hola", "pepe", "juan" }
jshell> Arrays.equals(s, a)
| Error:
| no suitable method found for equals(java.lang.String[],int[])
| method java.util.Arrays.equals(long[],long[]) is not applicable
| (argument mismatch; java.lang.String[]
cannot be converted to long[])
| method java.util.Arrays.equals(int[],int[]) is not applicable
| (argument mismatch; java.lang.String[]
cannot be converted to int[])
[...]
| method java.util.Arrays.<T>equals(T[],T[],
java.util.Comparator<? super T>) is not applicable
| (cannot infer type-variable(s) T
| (actual and formal argument lists differ in length))
| method java.util.Arrays.<T>equals(T[],int,int,T[],int,int,
java.util.Comparator<? super T>) is not applicable
| (cannot infer type-variable(s) T
| (actual and formal argument lists differ in length))
| Arrays.equals(s, a)
| ^-----------^
Se denomina dimensión de un array al número de índices que se necesitan para acceder a un elemento del array.
Los arrays que hemos visto hasta ahora necesitan un único índice para acceder a cada uno de sus elementos, por lo que su dimensión es 1.
A los arrays de dimensión 1 también se les denominan arrays unidimensionales.
Los arrays de dimensión mayor que 1 se denominan, genéricamente, arrays multidimensionales.
Esos arrays necesitan más de un índice para acceder a sus elementos individuales.
Los arrays multidimensionales más habituales son los de dimensión 2 (también llamados arrays bidimensionales) y de dimensión 3 (arrays tridimensionales).
En Java, los arrays multidimensionales se forman internamente haciendo que los elementos de un array sean, a su vez, otros arrays.
Es decir: en Java, los arrays multidimensionales son arrays de arrays.
Por tanto, al declarar un array tenemos que usar una sintaxis equivalente a la que hemos usado hasta ahora, pero usando tantas parejas de corchetes como sean necesarios.
Por ejemplo, la siguiente sentencia declara una variable
x
como una variable que puede contener un array
bidimensional donde sus elementos son enteros:
Cuando la variable apunte a un array de ese tipo, se podrá acceder a cada uno de sus elementos indicando dos índices, cada uno entre un par de corchetes:
En este caso, tenemos un array bidimensional.
Los arrays bidimensionales se pueden representar como una matriz de elementos organizados en filas y columnas.
Esos elementos se pueden almacenar por filas:
\begin{array}{c|ccccc} & 0 & 1 & 2 & 3 & \cdots \\ \hline{} 0 & \texttt{x[0][0]} & \texttt{x[0][1]} & \texttt{x[0][2]} & \cdots \\ 1 & \texttt{x[1][0]} & \texttt{x[1][1]} & \texttt{x[1][2]} & \cdots \\ 2 & \texttt{x[2][0]} & \texttt{x[2][1]} & \texttt{x[2][2]} & \cdots \\ \vdots & \vdots & \vdots & \ddots & \vdots \end{array}
O por columnas:
\begin{array}{c|ccccc} & 0 & 1 & 2 & 3 & \cdots \\ \hline{} 0 & \texttt{x[0][0]} & \texttt{x[1][0]} & \texttt{x[2][0]} & \cdots \\ 1 & \texttt{x[0][1]} & \texttt{x[1][1]} & \texttt{x[2][1]} & \cdots \\ 2 & \texttt{x[0][2]} & \texttt{x[1][2]} & \texttt{x[2][2]} & \cdots \\ \vdots & \vdots & \vdots & \ddots & \vdots \end{array}
Al crear un array multidimensional tenemos que indicar, al menos, el tamaño de la primera dimensión del array:
Podemos indicar el tamaño de más dimensiones, siempre en orden de izquierda a derecha:
La inicialización de un array multidimensional se hace de forma análoga a la de un array unidimensional:
En éste último caso, podemos hacer:
La inicialización de los subarrays (los arrays contenidos dentro de otros arrays) también se puede hacer de forma simplificada.
Es decir, en lugar de hacer:
se puede hacer:
Por ejemplo, la siguiente matriz:
\begin{bmatrix} 2 & 9 & 4 \\ 7 & 5 & 3 \\ 6 & 1 & 8 \end{bmatrix}
se puede representar por filas con el siguiente array bidimensional:
o de esta forma sintácticamente equivalente pero más fácil de leer:
Arrays.deepEquals
El método Arrays.equals
sirve para comprobar si dos arrays son iguales, pero sólo
funciona con arrays unidimensionales.
En cambio, el método Arrays.deepEquals
permite comprobar si dos arrays multidimensionales son
iguales:
jshell> int[][] x = new int[][] { null, new int[] { 4, 2, 3 }, null, null }
x ==> int[4][] { null, int[3] { 4, 2, 3 }, null, null }
jshell> int[][] y = new int[][] { null, new int[] { 4, 2, 3 }, null, null }
y ==> int[4][] { null, int[3] { 4, 2, 3 }, null, null }
jshell> Arrays.equals(x, y)
$3 ==> false
jshell> Arrays.deepEquals(x, y)
$4 ==> true