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 un NullPointerException.
  • 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:

Lecturas complementarias:

Temas:

  • La clase como molde y el objeto como instancia (instance): cada new crea un objeto con su propio estado. Ejemplo propio: una clase Thermometer con 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 = celsius no hace nada sin this.).

Sesión B — Referencias, heap, null · Identidad vs. igualdad

Lecturas:

Lecturas complementarias:

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 Thermometer y 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 un NullPointerException (Java indica qué expresión era null).
  • Garbage collector: un objeto sin referencias que lleguen a él se puede liberar. No hay free ni destructores.
  • Identidad vs. igualdad: == compara referencias; equals compara contenido cuando la clase lo define (String sí; 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.

  1. 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() con IO.println.
  2. 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.
  3. Provocar un NullPointerException y otro error de alias a propósito; leer el stack trace y encontrar la línea responsable.
  4. Depurar con jdb dentro del contenedor (compilar con javac -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 con ThermometerDemo.

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. Ver projects/README.md.

Preguntas de repaso

  1. Después de Thermometer a = new Thermometer(); Thermometer b = a; b.record(30);, ¿qué temperatura máxima tiene a? ¿Cuántos objetos hay en el heap?
  2. 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?
  3. ¿Por qué s1 == s2 puede ser false aunque s1.equals(s2) sea true? ¿Puede pasar al revés?
  4. Una clase tiene solo el constructor Thermometer(double initial). ¿Compila new Thermometer()? ¿Por qué?
  5. ¿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.

Aliasing.javaDescargar Aliasing.java
// 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());
}
Equality.javaDescargar Equality.java
// 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));
}
NullDemo.javaDescargar NullDemo.java
// 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");
}
ReadCompare.javaDescargar ReadCompare.java
// 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"));
}
Thermometer.javaDescargar Thermometer.java
/**
 * 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;
    }
}
ThermometerDemo.javaDescargar ThermometerDemo.java
// 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.

ShadowBug.javaDescargar ShadowBug.java
// 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.

ThermometerTest.javaDescargar ThermometerTest.java
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.

MakefileDescargar Makefile
# 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