Apuntes · Capítulo 2Objetos y clases
Clases, objetos y referencias
Clases, objetos y referencias
La semana pasada escribimos una primera clase, TransitCard, y vimos que un
objeto junta estado y comportamiento: campos que recuerdan y métodos que
responden a mensajes. También dejamos tres preguntas abiertas. ¿Qué pasa si se
usa una variable que no apunta a ningún objeto? ¿Qué diferencia hay entre que
dos objetos sean el mismo y que se parezcan? ¿Y qué guarda, exactamente,
una variable de tipo clase?
Este capítulo responde esas preguntas en dos partes. En la primera escribimos
una clase completa, con todas sus piezas: campos y sus valores por defecto,
métodos de instancia y this, varios constructores y el overloading. En la
segunda miramos la memoria: el stack y el heap, las referencias, los alias,
cómo se pasan los objetos a un método, null, el garbage collector y la
diferencia entre identidad e igualdad. Al final usamos un debugger para ver el
estado de un objeto mientras el programa corre.
Lectura. Este capítulo acompaña a Eck, Introduction to Programming Using Java (versión 9), secciones 5.1 Objects, Instance Methods, and Instance Variables (completa, con atención a 5.1.2 y 5.1.4), 5.2 Constructors and Object Initialization, la subsección 5.6.1 sobre
this, la 4.3.3 sobre overloading y la 2.3.3 sobreequalsenString. Para el laboratorio, la 3.2.3 Coding, Testing, Debugging. El código del capítulo está encode/week-02y se prueba conmake testdentro del contenedor del curso (env/run.sh).
Ruta de lectura.
- Antes de clase (obligatorio, 15–20 min): la introducción de Una clase completa:
Thermometery Stack y heap.- En clase: en la sesión A, el resto de Una clase completa: campos, métodos y
this, constructores y overloading; en la sesión B, Referencias, heap ynull, de Alias a Identidad frente a igualdad. En el laboratorio, Laboratorio: depurar conjdb.- Después de clase: los dos ejemplos resueltos (Ejemplo resuelto: usar la clase y Ejemplo resuelto: alias en
jshell), el Resumen y los Ejercicios. Para ampliar, las secciones de Eck de las notas Lectura.
Una clase completa: Thermometer
Nuestro ejemplo de esta semana es un termómetro que recuerda la temperatura
actual y también la mínima y la máxima registradas desde que se instaló, como
los termómetros de máximas y mínimas de una estación meteorológica.
Thermometer.java:
public class Thermometer {
private String place; // dónde está instalado
private double current; // grados Celsius
private double min;
private double max;
private int readings; // cuántas lecturas lleva, contando la inicial
/** Crea un termómetro en un lugar, con una primera lectura. */
public Thermometer(String place, double initial) {
this.place = place;
this.current = initial;
this.min = initial;
this.max = initial;
this.readings = 1;
}
/** Crea un termómetro sin lugar conocido. */
public Thermometer(double initial) {
this("sin lugar", initial);
}
/** Crea un termómetro sin lugar conocido que empieza en 0 °C. */
public Thermometer() {
this(0.0);
}
/** Registra una lectura nueva: cambia la actual y, si hace falta, la mínima o la máxima. */
public void record(double celsius) {
current = celsius;
if (celsius < min) {
min = celsius;
}
if (celsius > max) {
max = celsius;
}
readings++;
}
public String place() {
return place;
}
public double current() {
return current;
}
// min(), max() y readings() son iguales: devuelven su campo.
/** Diferencia entre la máxima y la mínima registradas. */
public double range() {
return max - min;
}
}
La clase es el molde; cada objeto creado con new Thermometer(...) es una
instancia (instance) con su propio estado. El termómetro de Lima y el del
laboratorio son dos instancias de la misma clase: el código de record se
escribió una vez, pero cada objeto tiene su current, su min y su max.
Vamos pieza por pieza.
Campos y valores por defecto
Los campos (fields) se declaran dentro de la clase y fuera de cualquier método. Cada objeto tiene su propia copia de cada campo, y la copia vive mientras viva el objeto, no mientras se ejecuta un método.
Cuando new reserva un objeto, todos sus campos empiezan en un valor por
defecto, antes de que corra el constructor:
| Tipo del campo | Valor por defecto |
|---|---|
int, long, short, byte |
0 |
double, float |
0.0 |
char |
el carácter de código 0 |
boolean |
false |
cualquier referencia (String, arreglos, clases) |
null |
Las variables locales, las que se declaran dentro de un método, son distintas: no tienen valor por defecto, y el compilador exige que tengan un valor antes de usarlas:
void main() {
int count;
IO.println(count + 1);
}
$ javac -Xlint:all Uninit.java
Uninit.java:3: error: variable count might not have been initialized
IO.println(count + 1);
^
1 error
En C, ese count tendría basura: lo que hubiera en esa posición del stack. En
Java el programa ni siquiera compila. Para los campos, Java no puede hacer ese
análisis (un campo se puede asignar desde cualquier método, en cualquier
orden), así que elige otro camino: los pone en cero.
Idea clave. Un campo nunca tiene basura: empieza en
0,falseonull. Una variable local no tiene valor hasta que se le asigna uno, y el compilador lo verifica. En ninguno de los dos casos se lee memoria vieja.
Métodos de instancia y this
Un método de instancia (instance method) se ejecuta siempre sobre un
objeto: el que recibió el mensaje, llamado el receptor (receiver). En
lima.record(22.0), el receptor es el objeto al que apunta lima. Dentro de
record, el nombre current se refiere al campo current de ese objeto.
¿Cómo sabe el método cuál es el receptor? Java le pasa una referencia
escondida, que dentro del método se llama this. Es lo que en C hacíamos a
mano con el parámetro struct card *c: this.current es el equivalente de
c->current. Casi nunca hace falta escribirlo, porque current a secas ya
significa this.current. Se escribe cuando hay que distinguir el campo de un
parámetro o variable local con el mismo nombre, como en el constructor:
public Thermometer(String place, double initial) {
this.place = place; // campo = parámetro
...
}
Los métodos de una clase suelen ser de dos clases:
- Consultas (queries): devuelven algo sobre el estado y no lo cambian.
current(),max()yrange()son consultas. Se pueden llamar las veces que se quiera, en cualquier orden, sin consecuencias. - Modificadores (commands): cambian el estado y, en general, no devuelven
nada (
void).recordes un modificador.
Separarlos ayuda a razonar sobre un programa: si un método se llama max(),
nadie espera que cambie el termómetro. Fíjense en que range() no tiene un
campo propio: se calcula a partir de otros dos. Quien la usa no sabe, ni
necesita saber, si el valor estaba guardado o se calculó.
Constructores
Un constructor (constructor) se llama igual que la clase y no tiene tipo
de retorno, ni siquiera void. Se ejecuta una sola vez por objeto, justo
después de que new reserva la memoria, y su trabajo es dejar los campos en un
estado con sentido. En Thermometer, la primera lectura es a la vez la actual,
la mínima y la máxima, y el contador empieza en 1.
Si una clase no declara ningún constructor, el compilador agrega uno
invisible, sin parámetros y que no hace nada: el constructor por defecto
(default constructor). Pero desaparece apenas escribimos un constructor
propio. Si Thermometer tuviera solo Thermometer(double initial), esto no
compilaría:
Thermometer t = new Thermometer();
Main.java:2: error: constructor Thermometer in class Thermometer cannot be applied to given types;
Thermometer t = new Thermometer();
^
required: double
found: no arguments
reason: actual and formal argument lists differ in length
1 error
El mensaje lo dice todo: el único constructor que existe pide un double
(required) y le dimos nada (found).
Overloading y this(...)
Nuestro Thermometer tiene tres constructores. Eso es posible gracias al
overloading: en una clase puede haber varios métodos, o
varios constructores, con el mismo nombre, siempre que sus listas de
parámetros sean distintas en número o en tipos. El compilador elige cuál usar
mirando los argumentos de cada llamada:
new Thermometer("Lima", 18.5) // (String, double)
new Thermometer(21.0) // (double)
new Thermometer() // ()
En el overloading, el tipo de retorno no cuenta para distinguir las versiones;
solo los parámetros. IO.println es un ejemplo que ya usaban sin saberlo: hay una
versión para cada tipo de argumento.
Los tres constructores hacen casi lo mismo. Para no repetir el código, uno
puede llamar a otro con this(...), que tiene que ser la primera
instrucción del constructor:
public Thermometer(double initial) {
this("sin lugar", initial); // delega en el constructor (String, double)
}
public Thermometer() {
this(0.0); // delega en el constructor (double)
}
Así, toda la inicialización real vive en un solo constructor. Si mañana agregamos un campo, se inicializa en un solo lugar.
Ejemplo resuelto: usar la clase
ThermometerDemo.java crea
termómetros con los tres constructores y les envía mensajes:
Thermometer lima = new Thermometer("Lima", 18.5);
lima.record(22.0);
lima.record(16.0);
IO.println(lima.place() + ": actual " + lima.current()
+ ", mín " + lima.min() + ", máx " + lima.max()
+ ", " + lima.readings() + " lecturas");
Thermometer lab = new Thermometer(21.0); // overloading: otro constructor
Thermometer fresh = new Thermometer(); // y otro más
IO.println(lab.place() + ": " + lab.current());
IO.println(fresh.place() + ": " + fresh.current());
// Cada objeto tiene su propio estado: registrar en lab no cambia lima.
lab.record(35.0);
IO.println("rango de lab: " + lab.range() + ", rango de lima: " + lima.range());
$ java -cp build ThermometerDemo
Lima: actual 16.0, mín 16.0, máx 22.0, 3 lecturas
sin lugar: 21.0
sin lugar: 0.0
rango de lab: 14.0, rango de lima: 6.0
Sigan el termómetro de Lima: empieza en 18.5 (mínima y máxima 18.5), sube a
22.0 (nueva máxima), baja a 16.0 (nueva mínima). Tres lecturas, rango
22.0 − 16.0 = 6.0. El de lab empieza en 21.0 y sube a 35.0: rango 14.0. Las
lecturas de uno no tocan al otro.
Error común. Escribir un “constructor” con tipo de retorno:
public class Thermometer { private double current; public void Thermometer(double initial) { // tiene tipo de retorno: es un método current = initial; } }Con
void, eso ya no es un constructor: es un método común que se llama igual que la clase (Java lo permite, aunque nadie debería hacerlo). La clase se queda con el constructor por defecto, y el error aparece lejos, donde se crea el objeto:Main.java:2: error: constructor Thermometer in class Thermometer cannot be applied to given types; Thermometer t = new Thermometer(25.0); ^ required: no arguments found: double reason: actual and formal argument lists differ in length 1 errorCuando un error dice que el constructor pide no arguments y ustedes escribieron uno con parámetros, busquen un
voidde más.
Error común. Un parámetro que se llama igual que un campo lo tapa: dentro del método, el nombre se refiere al parámetro. Eso se llama shadowing.
ShadowBug.java:class BuggyThermometer { private double current; BuggyThermometer(double current) { current = current; // asigna el parámetro a sí mismo; el campo queda en 0.0 } ... }$ java -cp build ShadowBug esperaba 25.0 y obtuve 0.0Compila sin una sola advertencia, incluso con
-Xlint:all. El campo nunca se asignó y se quedó en su valor por defecto. La corrección esthis.current = current;.
Referencias, heap y null
Stack y heap
Cuando un método se ejecuta, sus variables locales y sus parámetros viven en un stack frame, la parte del stack que le toca a esa llamada, igual que en C: el stack frame aparece al llamar al método y desaparece cuando el método termina. Los objetos, en cambio, viven en el heap, y duran mientras alguien pueda llegar a ellos.
Una variable de tipo clase vive en el stack, pero no contiene el objeto:
contiene una referencia (reference) a él. Después de ThermometerDemo, la
memoria se ve así:
Tres cosas para mirar en la figura:
limaylabson variables pequeñas: guardan solo la flecha, sin importar cuántos campos tenga el objeto.- Los campos primitivos (
current,min,max,readings) guardan su valor dentro del objeto. - El campo
placees de tipoString, así que también es una referencia: el texto vive en otro objeto del heap.
Si vienen de C, una referencia se parece mucho a un puntero a un struct
reservado con malloc. Las diferencias son las de la semana 1: no hay
aritmética de punteros, no se puede convertir un número en referencia, solo
puede apuntar a un objeto del tipo correcto (o a ninguno) y no hay free.
En Java. En HotSpot, una referencia ocupa 4 u 8 bytes según la configuración de la JVM, y cada objeto lleva además una cabecera con información para la JVM (entre otras cosas, cuál es su clase). No necesitan recordar esos números; sí que copiar una referencia es barato y nunca copia el objeto.
Alias
Como una variable guarda una referencia, asignar una variable a otra copia la referencia, no el objeto. Las dos variables quedan apuntando al mismo objeto: son alias (aliases).


Por eso Aliasing.java empieza así:
Thermometer a = new Thermometer("Lima", 18.0);
Thermometer b = a; // alias: dos variables, un solo objeto
b.record(30.0);
IO.println("a.max() = " + a.max() + " (a == b: " + (a == b) + ")");
a.max() = 30.0 (a == b: true)
Nadie le envió un mensaje a a, y sin embargo su máxima cambió. En un programa
corto se ve enseguida. En uno grande, con referencias guardadas en campos de
otros objetos y en listas, un alias olvidado produce errores difíciles de
encontrar: un objeto “cambia solo” porque otra parte del programa, que tenía
otra referencia al mismo objeto, lo modificó.
Idea clave.
=entre variables de tipo clase copia la flecha, no el objeto. Antes de modificar un objeto, pregúntense quién más tiene una referencia a él.
Paso de parámetros: siempre por valor
En Java, los argumentos se pasan siempre por valor: el parámetro es una
variable nueva del método llamado, que recibe una copia del valor del
argumento. Con un int, se copia el número. Con un objeto, el valor de la
variable es una referencia, así que se copia la referencia. Eso tiene dos
consecuencias, y Aliasing.java muestra las dos:
// Recibe una copia de la referencia: llega al mismo objeto y lo modifica.
void heatUp(Thermometer t) {
t.record(t.current() + 5);
}
// También recibe una copia de la referencia, pero le asigna otro objeto:
// solo cambia el parámetro, no la variable de quien llamó.
void replace(Thermometer t) {
t = new Thermometer("Cusco", 4.0);
t.record(-2.0);
}
heatUp(a);
IO.println("después de heatUp: " + a.place() + ", actual " + a.current());
replace(a);
IO.println("después de replace: " + a.place() + ", actual " + a.current());
después de heatUp: Lima, actual 35.0
después de replace: Lima, actual 35.0


heatUpusa su copia de la referencia para enviarle un mensaje al objeto. El objeto es el mismo que vemain, así que el cambio se ve afuera.replacele asigna al parámetro un objeto nuevo. Eso cambia la copia que tienereplace, no la variableademain, que sigue apuntando al termómetro de Lima. Cuandoreplacetermina, su stack frame desaparece y nadie recuerda el termómetro de Cusco.
Si vienen de C++, noten que Java no tiene paso por referencia (&): ningún
método puede hacer que una variable de quien lo llamó apunte a otro objeto.
Error común. Decir que “Java pasa los objetos por referencia”. Lo que se pasa, por valor, es la referencia. La prueba es
replace: si el objeto se pasara por referencia,ahabría terminado en Cusco.
null y el NullPointerException
Una variable de tipo referencia puede no apuntar a ningún objeto. Ese valor se
llama null. Es el valor por defecto de los campos de tipo referencia y se
puede asignar a mano: Thermometer t = null;.
Con una referencia null se puede hacer poco: compararla (t == null),
asignarla o pasarla a un método. Lo que no se puede es usarla para llegar a
un objeto, porque no hay objeto. Enviar un mensaje a través de null (t.max())
o leer un campo produce un NullPointerException, el error más común de
Java. NullDemo.java lo provoca a propósito:
void main() {
Thermometer ok = new Thermometer("Lima", 18.0);
IO.println(ok.place() + " tiene " + ok.place().length() + " letras");
Thermometer broken = new Thermometer(null, 18.0); // nada lo impide (todavía)
IO.println("lugar: " + broken.place());
IO.println(broken.place() + " tiene " + broken.place().length() + " letras");
}
$ java -cp build NullDemo
Lima tiene 4 letras
lugar: null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "Thermometer.place()" is null
at NullDemo.main(NullDemo.java:8)
Lean el stack trace de abajo hacia arriba como una dirección:
at NullDemo.main(NullDemo.java:8): el error ocurrió en el métodomain, en la línea 8 del archivoNullDemo.java.Cannot invoke "String.length()": se intentó enviar el mensajelength()…because the return value of "Thermometer.place()" is null: …a algo que eranull, y ese algo era lo que devolvióplace().
Desde Java 15, el mensaje dice qué expresión era null
(JEP 358). En una línea como la 8, con dos
llamadas a place() y una a length(), eso ahorra mucho tiempo. Fíjense
también en la línea 7: imprimir una referencia null con + no falla,
escribe null. Lo que falla es enviarle un mensaje.
En Java. Los nombres de las variables locales aparecen en el mensaje solo si el programa se compiló con información de depuración (
javac -g); sin ella, Java escribe algo como"<local2>". ElMakefilede esta semana compila con-gpor eso, y porque el debugger también la necesita.
El problema real está en la línea 6: un termómetro sin lugar no tiene sentido,
y la clase lo aceptó sin decir nada. Cuando el error salta, en la línea 8, el
dato malo ya lleva rato dentro del objeto. La semana 3 resuelve eso: el
constructor va a validar sus argumentos y a rechazar un null en el
momento en que alguien intente crearlo.
El garbage collector
En la animación del paso por valor, el termómetro de Cusco quedó sin ninguna referencia que llegue a él. Un objeto así es inalcanzable (unreachable): el programa no tiene forma de volver a usarlo. El garbage collector de la JVM recupera la memoria de esos objetos cuando le conviene; no sabemos cuándo, y no hace falta saberlo.
Un objeto se vuelve inalcanzable cuando se pierde la última referencia a él:
porque se le asignó otro objeto (o null) a la variable, o porque terminó el
método cuyo stack frame tenía la variable. Lo que cuenta es si se puede llegar
desde las variables vivas, siguiendo flechas, no si “alguien lo usa”.
Java no tiene free ni destructores. La otra cara de esto es que un objeto al
que todavía apunta algo no se libera, aunque el programa ya no lo
necesite. Si una lista guarda referencias a objetos viejos que nadie va a
volver a consultar, esos objetos siguen ocupando memoria: es la forma que
toman los memory leaks en Java.
Idea clave. El garbage collector libera los objetos a los que no se puede llegar. No libera los que siguen siendo alcanzables, aunque ya no sirvan.
Identidad frente a igualdad
La semana 1 vimos que == entre referencias pregunta si las dos flechas
llegan al mismo objeto. Eso es la identidad (identity). Muchas veces
queremos otra cosa: saber si dos objetos, quizá distintos, representan lo
mismo. Eso es la igualdad (equality), y en Java la responde el método
equals, cuando la clase lo define.
String define equals: compara los caracteres.
Equality.java:
String s1 = "Lima";
String s2 = "Lima";
String part = "Li";
String s3 = part + "ma"; // se arma mientras el programa corre
IO.println("s1 == s2: " + (s1 == s2));
IO.println("s1 == s3: " + (s1 == s3));
IO.println("s1.equals(s3): " + s1.equals(s3));
s1 == s2: true
s1 == s3: false
s1.equals(s3): true
¿Por qué s1 == s2 es true? Java guarda una sola copia de cada literal
de texto que aparece en el programa, así que s1 y s2 apuntan al mismo
objeto. s3, en cambio, se arma al ejecutar con +, y eso crea un objeto
nuevo con los mismos caracteres.
Nuestras clases todavía no definen equals. Cuando una clase no lo define,
hereda uno que hace exactamente lo mismo que ==:
Thermometer t1 = new Thermometer("Lima", 18.0);
Thermometer t2 = new Thermometer("Lima", 18.0);
IO.println("t1 == t2: " + (t1 == t2));
IO.println("t1.equals(t2): " + t1.equals(t2));
t1 == t2: false
t1.equals(t2): false
Decidir cuándo dos termómetros “son iguales” es una decisión de diseño (¿el
mismo lugar?, ¿las mismas lecturas?), y escribir un equals correcto tiene
sus trampas. Lo hacemos en la semana 5.
Error común. Comparar textos con
==. Con literales, a veces “funciona”, porque dos literales iguales son el mismo objeto; con un texto que llega del teclado, de un archivo o de una concatenación, no:// ReadCompare.java String city = IO.readln("Ciudad: "); IO.println("city == \"Lima\": " + (city == "Lima")); IO.println("city.equals(\"Lima\"): " + city.equals("Lima"));$ echo Lima | java -cp build ReadCompare Ciudad: city == "Lima": false city.equals("Lima"): trueEl usuario escribió exactamente
Limay==dicefalse. Para comparar textos, siempreequals.
Ejemplo resuelto: alias en jshell
jshell muestra cada objeto como Clase@número. Ese número no es una
dirección de memoria, pero sirve para ver si dos variables apuntan al mismo
objeto. Dentro de code/week-02:
jshell> /open Thermometer.java
jshell> Thermometer a = new Thermometer()
a ==> Thermometer@31221be2
jshell> Thermometer b = a
b ==> Thermometer@31221be2
jshell> b.record(30)
jshell> double m = a.max()
m ==> 30.0
jshell> boolean same = a == b
same ==> true
jshell> String s1 = "Lima"
s1 ==> "Lima"
jshell> String s3 = "Li" + s1.substring(2)
s3 ==> "Lima"
jshell> boolean r1 = s1 == s3
r1 ==> false
jshell> boolean r2 = s1.equals(s3)
r2 ==> true
aybmuestran el mismo@31221be2: es un solo objeto.b.record(30)es un modificadorvoid, así quejshellno muestra nada; peroa.max()ya vale 30.0.s1ys3se imprimen igual,"Lima", y sin embargo son dos objetos distintos. Mirar la salida no basta para saber si dos referencias son idénticas.
Laboratorio: depurar con jdb
Un debugger permite detener un programa en una línea, mirar el valor de
las variables y de los campos, y avanzar instrucción por instrucción. El JDK
trae uno de línea de comandos,
jdb,
que funciona dentro del contenedor. Los editores (VS Code, IntelliJ) tienen
uno gráfico que hace lo mismo con clics; las ideas son las mismas.
El programa tiene que estar compilado con -g para que el debugger conozca
los nombres de las variables locales; el Makefile de la semana ya lo hace.
Pongamos un breakpoint en la primera línea de record
(línea 33 de Thermometer.java) y ejecutemos ThermometerDemo:
$ make
$ jdb -classpath build ThermometerDemo
Initializing jdb ...
> stop at Thermometer:33
Deferring breakpoint Thermometer:33.
It will be set after the class is loaded.
> run
run ThermometerDemo
...
VM Started: Set deferred breakpoint Thermometer:33
Breakpoint hit: "thread=main", Thermometer.record(), line=33 bci=0
main[1] where
[1] Thermometer.record (Thermometer.java:33)
[2] ThermometerDemo.main (ThermometerDemo.java:4)
main[1] print celsius
celsius = 22.0
main[1] dump this
this = {
place: "Lima"
current: 18.5
min: 18.5
max: 18.5
readings: 1
}
El programa se detuvo al entrar a record, antes de ejecutar la línea 33.
wheremuestra el call stack: estamos enrecord, que fue llamado desde la línea 4 deThermometerDemo(lima.record(22.0)).print celsiusmuestra el parámetro.dump thismuestra todos los campos del receptor: el termómetro de Lima, todavía con su primera lectura.
Ahora avanzamos línea por línea con next y volvemos a mirar el objeto:
main[1] next
Step completed: "thread=main", Thermometer.record(), line=34 bci=5
main[1] next
Step completed: "thread=main", Thermometer.record(), line=37 bci=19
main[1] next
Step completed: "thread=main", Thermometer.record(), line=38 bci=28
main[1] dump this
this = {
place: "Lima"
current: 22.0
min: 18.5
max: 18.5
readings: 1
}
main[1] clear Thermometer:33
Removed: breakpoint Thermometer:33
main[1] cont
Lima: actual 16.0, mín 16.0, máx 22.0, 3 lecturas
sin lugar: 21.0
sin lugar: 0.0
rango de lab: 14.0, rango de lima: 6.0
The application exited
De la línea 34 saltamos a la 37: como 22.0 no es menor que 18.5, el cuerpo del
primer if no se ejecutó. En la 38 estamos a punto de actualizar max: el
objeto ya tiene current en 22.0, pero max sigue en 18.5. clear quita el
breakpoint y cont deja que el programa termine.
Comando de jdb |
Qué hace |
|---|---|
stop at Clase:línea |
Breakpoint en esa línea |
run |
Arranca el programa |
where |
Call stack: en qué método estamos y quién lo llamó |
print expr |
Valor de una variable o expresión |
dump expr |
Todos los campos de un objeto |
locals |
Todas las variables locales del stack frame actual |
next |
Ejecuta la línea actual sin entrar a los métodos que llama |
step |
Ejecuta la línea actual entrando a los métodos que llama |
cont |
Sigue hasta el próximo breakpoint o el final |
clear Clase:línea |
Quita un breakpoint |
Las actividades del laboratorio:
- Escribir una clase pequeña con overloading de constructores (uno que
delegue en el otro con
this(...)), consultas y modificadores, y probarla desde un archivo compacto conIO.println. - Dibujar a mano el stack y el heap después de cada línea de un programa
corto con alias, y comprobar el dibujo en
jshell. - Provocar a propósito un
NullPointerExceptiony un error de alias; leer el stack trace y encontrar la línea responsable. - Depurar con
jdb(o con el debugger del editor): poner un breakpoint dentro de un método, avanzar connexty ver cómo cambian los campos del receptor condump this.
Lectura. Eck, 3.2.3 Coding, Testing, Debugging, sobre cómo buscar errores con método, y 2.6.6 About jshell.
El código de la semana y make test
Dentro del contenedor, en code/week-02:
$ make test
mkdir -p build
javac -Xlint:all -Werror -g -cp /opt/junit/junit.jar -d build Aliasing.java Equality.java NullDemo.java ReadCompare.java ShadowBug.java ThermometerDemo.java Thermometer.java ThermometerTest.java
touch build/.stamp
== ThermometerDemo
Lima: actual 16.0, mín 16.0, máx 22.0, 3 lecturas
...
== NullDemo (termina con NullPointerException a propósito)
Lima tiene 4 letras
lugar: null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "Thermometer.place()" is null
at NullDemo.main(NullDemo.java:8)
...
├─ JUnit Jupiter ✔
│ └─ ThermometerTest ✔
│ ├─ firstReadingIsMinAndMax() ✔
│ ├─ noArgConstructorStartsAtZero() ✔
│ └─ recordUpdatesMinAndMax() ✔
...
[ 3 tests successful ]
[ 0 tests failed ]
| Archivo | Qué muestra |
|---|---|
Thermometer.java |
La clase completa: campos, tres constructores, consultas y un modificador |
ThermometerDemo.java |
Crear objetos con cada constructor y enviarles mensajes |
ShadowBug.java |
Shadowing: un parámetro que tapa al campo |
Aliasing.java |
Alias y paso de parámetros por valor |
Equality.java |
== frente a equals |
ReadCompare.java |
== con un texto leído de la entrada |
NullDemo.java |
Un NullPointerException y su mensaje (termina con error a propósito) |
ThermometerTest.java |
Pruebas JUnit de muestra; a escribirlas se aprende la semana 3 |
NullDemo es el único programa que termina con una excepción; el Makefile
lo da por bueno solo si falla, que es lo que tiene que hacer.
Resumen
- Una clase es el molde; cada instancia tiene su propio estado. Los
campos empiezan en
0,falseonull; las variables locales no tienen valor por defecto y el compilador exige asignarlas. - Un método de instancia se ejecuta sobre un receptor, al que se llega
con
this. Conviene separar consultas (devuelven, no cambian) de modificadores (cambian, no devuelven). - Un constructor se llama como la clase y no tiene tipo de retorno. El
constructor por defecto desaparece al escribir uno propio. El
overloading permite varios constructores;
this(...)evita repetir código. - Las variables locales viven en el stack; los objetos, en el heap. Una variable de tipo clase guarda una referencia.
=copia la referencia: dos variables pueden ser alias del mismo objeto.- Java pasa los argumentos por valor; con objetos, se copia la referencia. Un método puede modificar el objeto, pero no cambiar a qué objeto apunta la variable de quien llamó.
nullno apunta a nada. Enviarle un mensaje produce unNullPointerException, cuyo mensaje dice qué expresión eranully en qué línea.- El garbage collector libera los objetos inalcanzables. No hay
free. ==compara identidad (las flechas);equalscompara igualdad cuando la clase lo define. Los textos se comparan conequals.- Un debugger detiene el programa y muestra las variables y los campos:
stop at,run,next,print,dump.
Ejercicios
-
Sin ejecutarlo, dibujen el stack y el heap al final de este fragmento y digan qué imprime. ¿Cuántos objetos
Thermometerhay en el heap? ¿Alguno es inalcanzable?Thermometer x = new Thermometer("Piura", 30.0); Thermometer y = new Thermometer("Puno", 5.0); Thermometer z = x; z.record(34.0); x = y; x.record(-3.0); IO.println(z.max() + " " + y.min() + " " + (x == y)); -
Una clase
Probetiene solo el constructorProbe(double initial). ¿Compilanew Probe()? ¿Y si se agrega un segundo constructor sin parámetros que llame athis(20.0)? ¿Qué pasa sithis(20.0)no es la primera línea? -
Agreguen a
Thermometeruna consultaaverage()que devuelva el promedio de todas las lecturas registradas. ¿Qué campo nuevo necesitan? ¿Qué métodos tienen que cambiar? Agreguen una prueba enThermometerTest.java, copiando el estilo de las que hay, y verifiquen conmake test. -
Escriban un método
void swap(Thermometer p, Thermometer q)que intente intercambiar los objetos a los que apuntan dos variables demain. Pruébenlo. ¿Funciona? Expliquen por qué con un dibujo del stack. ¿Pueden escribir un método que intercambie los estados de dos termómetros? -
En
jshell, evalúenString a = "uni",String b = "UNI".toLowerCase(),a == b,a.equals(b)ya.equals(null). Después,String c = nullyc.equals(a). Expliquen cada resultado. -
Modifiquen
NullDemo.javapara que elnullesté en una variable local (Thermometer t = null;) y llamen at.max(). Copien el mensaje delNullPointerException. Compilen después sin-g(javac -d out NullDemo.java Thermometer.java) y comparen el mensaje. -
Este constructor tiene dos errores que compilan sin advertencias. Encuentren ambos y digan qué valores quedan en los campos después de
new Clock(10, 30):public class Clock { private int hours; private int minutes; public Clock(int hours, int minutes) { hours = hours; this.minutes = minutes; } public void Clock() { this.hours = 0; } } -
Un programa guarda en una lista todos los
Thermometerque crea, para mostrarlos al final, y a la mitad deja de usar la mitad de ellos. ¿Puede el garbage collector liberar esos objetos? ¿Qué habría que cambiar para que pueda? -
Usen
jdbconThermometerDemo: pongan un breakpoint en la línea del constructorThermometer(String, double)que asignathis.miny ejecutendump this. ¿Qué valores tienen los campos que todavía no se asignaron? Relacionen la respuesta con los valores por defecto. -
Usen
stepen lugar denextsobre la líneathis(0.0);del constructor sin parámetros (pongan el breakpoint ahí y ejecutenThermometerDemo). ¿Por qué constructores pasa la ejecución, y en qué orden? -
Escriban
Ticket, la entrada de un concierto, con un código, el nombre de quien la compró y un booleanoused. Den dos constructores (uno recibe ese nombre; el otro no y usa"sin nombre"), una consultaisUsed()y un modificadoruse()que devuelvafalsesi la entrada ya se había usado. Pruébenla desde un archivo compacto y muestren un caso de alias en que usar la entrada a través de una variable la “gasta” para la otra. -
Lean la subsección 5.1.2 de Eck. Él compara las variables de objeto con las de tipo primitivo en una asignación y en
==. Escriban, con sus palabras y en no más de cinco líneas, la regla general que une esos casos.