Semana 2 — Clases, objetos y referencias
Objetivos
- Escribir una clase completa: campos (fields), métodos de instancia, constructores y overloading.
- Explicar qué guarda una variable de tipo clase: una referencia (reference) a un objeto que vive en el heap.
- Distinguir identidad (
==) de igualdad (equals) y reconocer de dónde sale unNullPointerException. - Usar un debugger para seguir la ejecución y ver el estado de los objetos.
Apuntes de la semana: notes/week-02.md · Código: code/week-02
Sesión A — Clases, campos, métodos y constructores
Lecturas:
- Eck, sec. 5.1 Objects, Instance Methods, and Instance Variables, subsecciones 5.1.1 a 5.1.3.
- Eck, sec. 5.2 Constructors and Object Initialization, subsecciones 5.2.1 y 5.2.2.
- Eck, subsec. 5.6.1 The Special Variable this y subsec. 4.3.3 Overloading.
Lecturas complementarias:
- dev.java, Classes and Objects: Creating Classes, Defining Methods y Providing Constructors for your Classes.
Temas:
- La clase como molde y el objeto como instancia (instance): cada
newcrea un objeto con su propio estado. Ejemplo propio: una claseThermometercon la temperatura actual, la mínima y la máxima registradas. - Campos de instancia y sus valores por defecto (
0,false,null). Diferencia con las variables locales, que no tienen valor por defecto y el compilador exige inicializar. - Métodos de instancia: el objeto receptor y
this. Métodos que consultan (devuelven un valor) frente a métodos que modifican el estado. - Constructores: mismo nombre que la clase, sin tipo de retorno. El constructor por defecto desaparece apenas se escribe uno. Overloading de constructores y
this(...)para no repetir código. - Error común: escribir
void Thermometer()y creer que es un constructor (es un método común); o un parámetro que se llama igual que el campo y lo tapa, lo que se llama shadowing (celsius = celsiusno hace nada sinthis.).
Sesión B — Referencias, heap, null · Identidad vs. igualdad
Lecturas:
- Eck, subsec. 5.1.2 Fundamentals of Objects (relectura con atención a referencias,
nully==) y 5.1.4 Arrays and Objects. - Eck, subsec. 5.2.3 Garbage Collection.
- Eck, subsec. 2.3.3 Operations on Strings (
equalsenString).
Lecturas complementarias:
- JLS 25, §15.21.3 Reference Equality Operators == and !=.
- Martin Fowler, Aliasing Bug: por qué compartir referencias a objetos mutables produce errores difíciles de ver.
Temas:
- Stack y heap: las variables locales viven en el stack frame del método; los objetos, en el heap. Una variable de tipo clase guarda una referencia, no el objeto. Comparación con los punteros de C: no hay aritmética de punteros ni
free. - Asignar una referencia no copia el objeto: dos variables pueden apuntar al mismo objeto (alias). Paso de parámetros: Java pasa siempre por valor, y el valor de una variable de objeto es una referencia. Ejemplo: un método que recibe un
Thermometery lo modifica, frente a uno que le asigna un objeto nuevo al parámetro. null: la referencia que no apunta a nada. Leer el stack trace de unNullPointerException(Java indica qué expresión eranull).- Garbage collector: un objeto sin referencias que lleguen a él se puede liberar. No hay
freeni destructores. - Identidad vs. igualdad:
==compara referencias;equalscompara contenido cuando la clase lo define (Stringsí; nuestras clases todavía no, eso se ve en la semana 5). - Error común: comparar textos con
==. A veces “funciona” con literales y falla con un texto leído del teclado o construido en tiempo de ejecución.
Laboratorio — Primeras clases y depuración
Código inicial en code/week-02/: la clase Thermometer y demostraciones en archivos fuente compactos con void main() (ThermometerDemo, ShadowBug, Aliasing, Equality, ReadCompare, NullDemo). make test compila con -g y corre las demostraciones y las pruebas de muestra.
- Escribir una clase pequeña que haga overloading de su constructor (dos versiones) y tenga métodos de consulta y de modificación; probarla desde un
void main()conIO.println. - Dibujar a mano el estado de stack y heap después de cada línea de un programa corto con alias, y verificarlo en
jshell. - Provocar un
NullPointerExceptiony otro error de alias a propósito; leer el stack trace y encontrar la línea responsable. - Depurar con
jdbdentro del contenedor (compilar conjavac -g;stop at,run,next,step,print,dump) o con el debugger del editor: poner un breakpoint y ver cómo cambian los campos del objeto. Los apuntes traen una sesión completa conThermometerDemo.
Referencia: Eck, subsec. 3.2.3 Coding, Testing, Debugging y 2.6.6 About jshell.
Entregas
- P0 en curso (calentamiento: programas cortos y el producto de la bodega como primera clase). Entrega en semana 3. Lo de esta semana (constructores,
this, métodos de consulta y de modificación) es justo lo que pide esa primera clase. Verprojects/README.md.
Preguntas de repaso
- Después de
Thermometer a = new Thermometer(); Thermometer b = a; b.record(30);, ¿qué temperatura máxima tienea? ¿Cuántos objetos hay en el heap? - Un método recibe un objeto como parámetro y le asigna
new ...al parámetro. ¿Cambia el objeto del que llamó? ¿Y si en lugar de eso llama a un método que modifica un campo? - ¿Por qué
s1 == s2puede serfalseaunques1.equals(s2)seatrue? ¿Puede pasar al revés? - Una clase tiene solo el constructor
Thermometer(double initial). ¿Compilanew Thermometer()? ¿Por qué? - ¿Cuándo puede el garbage collector liberar un objeto? ¿Qué pasa si una lista guarda una referencia a un objeto que ya no usamos?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (6)Errores (1)Pruebas (1)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Alias y paso de parámetros: Java pasa siempre por valor, y el valor de una
// variable de objeto es una referencia.
// 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);
}
void main() {
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) + ")");
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());
}// Identidad (==) frente a igualdad (equals).
void main() {
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));
// Nuestra clase todavía no define equals: hereda el de Object, que usa ==.
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));
}// Termina con un NullPointerException a propósito, para leer su mensaje.
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");
}// Comparar con == un texto leído del teclado. Pruébenlo con: echo Lima | java -cp build ReadCompare
void main() {
String city = IO.readln("Ciudad: ");
IO.println("city == \"Lima\": " + (city == "Lima"));
IO.println("city.equals(\"Lima\"): " + city.equals("Lima"));
}/**
* Un termómetro que recuerda la temperatura actual y la mínima y la máxima
* registradas desde que se creó.
*/
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;
}
public double min() {
return min;
}
public double max() {
return max;
}
public int readings() {
return readings;
}
/** Diferencia entre la máxima y la mínima registradas. */
public double range() {
return max - min;
}
}// Crea termómetros con cada constructor y les envía mensajes.
void main() {
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());
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Un error que compila sin advertencias: el parámetro tapa al campo (shadowing).
class BuggyThermometer {
private double current;
BuggyThermometer(double current) {
current = current; // asigna el parámetro a sí mismo; el campo queda en 0.0
}
double current() {
return current;
}
}
void main() {
BuggyThermometer t = new BuggyThermometer(25.0);
IO.println("esperaba 25.0 y obtuve " + t.current());
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
// Pruebas de muestra. Cómo escribirlas se ve en la semana 3.
class ThermometerTest {
@Test
void firstReadingIsMinAndMax() {
Thermometer t = new Thermometer("Lima", 18.5);
assertEquals(18.5, t.min());
assertEquals(18.5, t.max());
}
@Test
void recordUpdatesMinAndMax() {
Thermometer t = new Thermometer("Lima", 18.5);
t.record(22.0);
t.record(16.0);
assertEquals(16.0, t.current());
assertEquals(16.0, t.min());
assertEquals(22.0, t.max());
assertEquals(3, t.readings());
}
@Test
void noArgConstructorStartsAtZero() {
Thermometer t = new Thermometer();
assertEquals("sin lugar", t.place());
assertEquals(0.0, t.current());
}
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 2: clases, referencias e identidad.
# make compila todo a build/ (con -g, para poder depurar con jdb)
# make test corre las demostraciones y las pruebas JUnit
# make clean borra build/
JAVAC = javac
JFLAGS = -Xlint:all -Werror -g
SRC = $(wildcard *.java)
DEMOS = ThermometerDemo ShadowBug Aliasing Equality
all: build/.stamp
build/.stamp: $(SRC)
mkdir -p build
$(JAVAC) $(JFLAGS) -cp $(JUNIT_JAR) -d build $(SRC)
touch $@
test: all
@for d in $(DEMOS); do echo "== $$d"; java -cp build $$d || exit 1; done
@echo "== ReadCompare (lee Lima de la entrada)"; echo Lima | java -cp build ReadCompare
@echo "== NullDemo (termina con NullPointerException a propósito)"; ! java -cp build NullDemo
java -jar $(JUNIT_JAR) execute --class-path build --scan-class-path \
--disable-banner --disable-ansi-colors --details=tree
clean:
rm -rf build
.PHONY: all test clean