04
Objetos y clases

Arreglos, ArrayList y diseño con clases

Objetos y clases

Semana 4 — Arreglos, ArrayList y diseño con clases

Objetivos

  • Usar arreglos y ArrayList para guardar colecciones de objetos, y elegir entre ellos.
  • Entender que un arreglo de objetos es un arreglo de referencias, y qué consecuencias tiene.
  • Pasar de un enunciado a un diseño: identificar clases, responsabilidades y colaboraciones.
  • Leer y dibujar diagramas de clases UML sencillos.

Apuntes de la semana: notes/week-04.md · Código: code/week-04

Sesión A — Arreglos y ArrayList

Lecturas:

Lecturas complementarias:

  • API de Java 25: ArrayList (métodos add, get, set, remove, size, contains, indexOf).

Temas:

  • Arreglos en Java frente a C: conocen su longitud, se verifica el índice (ArrayIndexOutOfBoundsException), se crean con new y viven en el heap.
  • Un arreglo de objetos guarda referencias: new Song[10] crea diez null, no diez canciones. Arreglos parcialmente llenos y el contador que los acompaña.
  • Por qué un arreglo dinámico: crecer copiando a uno más grande. ArrayList hace eso por nosotros.
  • ArrayList<Song>: el tipo entre ángulos dice qué guarda la lista. Por ahora basta con leerlo así; los genéricos (generics) se estudian en la semana 10.
  • Clases wrapper y autoboxing: ArrayList<Integer>, no ArrayList<int>.
  • Recorridos con for clásico y con for-each. Ejemplo propio: una Playlist que encapsula su ArrayList<Song> (Song es un record) y ofrece add, totalSeconds, longest y removeShorterThan; ArrayPlaylist muestra a mano el arreglo que crece.
  • contains e indexOf comparan con equals: con un record, por valor.
  • Pruebas: @BeforeEach, assertSame y assertNull.
  • Error común: borrar elementos dentro de un for-each (ConcurrentModificationException, o peor, un elemento que se salta sin aviso); y list.remove(1) en un ArrayList<Integer>, que borra la posición 1 y no el valor 1.

Sesión B — Diseñar con clases: responsabilidades, UML de clases

Lecturas:

Lecturas complementarias (Eck no trata UML):

Temas:

  • Del enunciado al diseño: sustantivos como candidatos a clase, verbos como candidatos a método. Descartar los que son solo un atributo.
  • Responsabilidades: qué sabe y qué hace cada clase. Una clase con una responsabilidad clara es más fácil de probar y de cambiar.
  • Tarjetas CRC en grupo: ejemplo propio con una biblioteca (libro, ejemplar, socio, préstamo).
  • Diagrama de clases UML: clase con campos y métodos, visibilidad (+, -), asociación, multiplicidad (1, 0..*), agregación y composición. La herencia se agrega en la semana 5.
  • Error común: la god class que lo hace todo, y su contrario, clases que son solo datos con getters y setters mientras la lógica vive en main.

Laboratorio — Modelar un dominio con pruebas

Código de referencia en code/week-04/ (music/ y library/). El enunciado del laboratorio (un gimnasio con clases grupales, cupos e inscripciones) está al final de los apuntes.

  1. Dado un enunciado corto (un dominio distinto del proyecto), hacer tarjetas CRC y un diagrama de clases en Mermaid.
  2. Implementar dos o tres de esas clases con ArrayList encapsulado y sus invariantes.
  3. Escribir las pruebas JUnit antes o junto con cada método: casos normales, lista vacía, elemento repetido, límites.
  4. Revisar en parejas: ¿alguna clase devuelve su lista interna? ¿Algún método no está en la clase que tiene los datos?

Entregas

  • P1 en curso (Inventario). Entrega en semana 6. Esta semana da lo que falta: el inventario guarda sus productos en un ArrayList encapsulado, y el diagrama de clases del laboratorio sirve como primer diseño del proyecto.

Preguntas de repaso

  1. ¿Qué contiene new Song[3] justo después de crearse? ¿Y new int[3]?
  2. ¿Por qué ArrayList<int> no compila? ¿Qué hace el compilador con list.add(5) en un ArrayList<Integer>?
  3. En un ArrayList<Integer> con [5, 1, 7], ¿qué queda después de list.remove(1)? ¿Cómo se borra el valor 1?
  4. Dibujen el diagrama de clases de la biblioteca de la sesión B. ¿Qué multiplicidad tiene la asociación entre socio y préstamo?
  5. ¿En qué clase pondrían el método que calcula si un préstamo está vencido? Justifiquen según quién tiene los datos.

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (13)Pruebas (4)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

BoxingDemo.javaDescargar BoxingDemo.java
import java.util.ArrayList;

// ArrayList guarda objetos: los int se envuelven en Integer (autoboxing).
public class BoxingDemo {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(5);                        // el compilador escribe list.add(Integer.valueOf(5))
        list.add(1);
        list.add(7);
        IO.println("lista: " + list);

        list.remove(1);                     // remove(int index): quita la posición 1
        IO.println("después de remove(1): " + list);

        list.add(1);
        IO.println("después de add(1): " + list);
        list.remove(Integer.valueOf(1));    // remove(Object o): quita el valor 1
        IO.println("después de remove(Integer.valueOf(1)): " + list);

        int first = list.get(0);            // unboxing: Integer a int
        IO.println("first + 1 = " + (first + 1));
    }
}
RemoveInLoop.javaDescargar RemoveInLoop.java
import java.util.ArrayList;

// Quitar elementos dentro de un for-each: termina con una excepción.
public class RemoveInLoop {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("alice");
        names.add("bob");
        names.add("carol");
        names.add("dave");
        for (String n : names) {
            if (n.startsWith("b")) {
                names.remove(n);
            }
        }
        IO.println(names);
    }
}
library/Book.javaDescargar library/Book.java
package library;

/** Un título del catálogo. Un libro puede tener varios ejemplares. */
public record Book(String isbn, String title, String author) {

    public Book {
        if (isbn == null || isbn.isBlank()) {
            throw new IllegalArgumentException("el libro necesita un ISBN");
        }
    }
}
library/Copy.javaDescargar library/Copy.java
package library;

/** Un ejemplar físico de un libro, con su código de inventario. */
public class Copy {
    private final String code;
    private final Book book;
    private boolean onLoan = false;

    public Copy(String code, Book book) {
        this.code = code;
        this.book = book;
    }

    public String code() {
        return code;
    }

    public Book book() {
        return book;
    }

    public boolean isAvailable() {
        return !onLoan;
    }

    // Acceso de paquete: solo Library y Loan, del mismo paquete, cambian el estado.
    void markOnLoan() {
        onLoan = true;
    }

    void markReturned() {
        onLoan = false;
    }
}
library/Library.javaDescargar library/Library.java
package library;

import java.time.LocalDate;
import java.util.ArrayList;

/** La biblioteca: conoce sus ejemplares y sus socios, y registra préstamos. */
public class Library {
    public static final int LOAN_DAYS = 14;

    private final ArrayList<Copy> copies = new ArrayList<>();
    private final ArrayList<Member> members = new ArrayList<>();

    public void addCopy(Copy copy) {
        copies.add(copy);
    }

    public void addMember(Member member) {
        members.add(member);
    }

    /** Busca un ejemplar por su código; null si no existe. */
    public Copy findCopy(String code) {
        for (Copy c : copies) {
            if (c.code().equals(code)) {
                return c;
            }
        }
        return null;
    }

    /** Cuántos ejemplares de ese libro están disponibles. */
    public int availableCopies(Book book) {
        int n = 0;
        for (Copy c : copies) {
            if (c.book().equals(book) && c.isAvailable()) {
                n++;
            }
        }
        return n;
    }

    /**
     * Presta un ejemplar disponible a un socio que todavía puede pedir libros.
     * El préstamo vence LOAN_DAYS días después de today.
     */
    public Loan lend(Copy copy, Member member, LocalDate today) {
        if (!copy.isAvailable()) {
            throw new IllegalArgumentException("el ejemplar " + copy.code() + " ya está prestado");
        }
        if (!member.canBorrow()) {
            throw new IllegalArgumentException(member.name() + " ya tiene " + Member.MAX_LOANS + " préstamos");
        }
        Loan loan = new Loan(copy, member, today.plusDays(LOAN_DAYS));
        copy.markOnLoan();
        member.addLoan(loan);
        return loan;
    }

    public void giveBack(Loan loan) {
        if (loan.isReturned()) {
            throw new IllegalArgumentException("el préstamo ya se devolvió");
        }
        loan.markReturned();
        loan.member().removeLoan(loan);
    }
}
library/LibraryDemo.javaDescargar library/LibraryDemo.java
package library;

import java.time.LocalDate;

public class LibraryDemo {
    public static void main(String[] args) {
        Book tales = new Book("0001", "Cuentos del Rímac", "Rosa Quispe");
        Library lib = new Library();
        lib.addCopy(new Copy("B-001", tales));
        lib.addCopy(new Copy("B-002", tales));
        Member alice = new Member("S-17", "Alice");
        lib.addMember(alice);

        LocalDate today = LocalDate.of(2026, 9, 1);
        Loan loan = lib.lend(lib.findCopy("B-001"), alice, today);
        IO.println("vence el " + loan.due() + "; disponibles: " + lib.availableCopies(tales));
        IO.println("¿vencido el 10 de septiembre? " + loan.isOverdue(LocalDate.of(2026, 9, 10)));
        IO.println("¿vencido el 20 de septiembre? " + loan.isOverdue(LocalDate.of(2026, 9, 20)));
        lib.giveBack(loan);
        IO.println("devuelto; disponibles: " + lib.availableCopies(tales)
                + ", préstamos de " + alice.name() + ": " + alice.activeLoanCount());
    }
}
library/Loan.javaDescargar library/Loan.java
package library;

import java.time.LocalDate;

/** Un préstamo: qué ejemplar, a quién, y hasta cuándo. */
public class Loan {
    private final Copy copy;
    private final Member member;
    private final LocalDate due;
    private boolean returned = false;

    Loan(Copy copy, Member member, LocalDate due) {
        this.copy = copy;
        this.member = member;
        this.due = due;
    }

    public Copy copy() {
        return copy;
    }

    public Member member() {
        return member;
    }

    public LocalDate due() {
        return due;
    }

    public boolean isReturned() {
        return returned;
    }

    /** Vencido: no se devolvió y la fecha límite ya pasó. Loan tiene los datos, así que decide Loan. */
    public boolean isOverdue(LocalDate today) {
        return !returned && today.isAfter(due);
    }

    void markReturned() {
        returned = true;
        copy.markReturned();
    }
}
library/Member.javaDescargar library/Member.java
package library;

import java.time.LocalDate;
import java.util.ArrayList;

/** Un socio de la biblioteca. Invariante: nunca tiene más de MAX_LOANS préstamos activos. */
public class Member {
    public static final int MAX_LOANS = 3;

    private final String id;
    private final String name;
    private final ArrayList<Loan> activeLoans = new ArrayList<>();

    public Member(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String id() {
        return id;
    }

    public String name() {
        return name;
    }

    public int activeLoanCount() {
        return activeLoans.size();
    }

    public boolean canBorrow() {
        return activeLoans.size() < MAX_LOANS;
    }

    /** Cuántos de sus préstamos están vencidos hoy. */
    public int overdueCount(LocalDate today) {
        int n = 0;
        for (Loan loan : activeLoans) {
            if (loan.isOverdue(today)) {
                n++;
            }
        }
        return n;
    }

    void addLoan(Loan loan) {
        activeLoans.add(loan);
    }

    void removeLoan(Loan loan) {
        activeLoans.remove(loan);
    }
}
music/ArrayPlaylist.javaDescargar music/ArrayPlaylist.java
package music;

/**
 * Lo que hace un ArrayList por dentro: un arreglo parcialmente lleno, un
 * contador y, cuando se llena, una copia a un arreglo del doble de tamaño.
 */
public class ArrayPlaylist {
    private Song[] songs = new Song[2];     // capacidad inicial pequeña, para verlo crecer
    private int count = 0;                  // songs[0..count-1] están en uso

    public void add(Song song) {
        if (count == songs.length) {
            grow();
        }
        songs[count] = song;
        count++;
    }

    public int size() {
        return count;
    }

    public int capacity() {
        return songs.length;
    }

    public Song get(int index) {
        if (index < 0 || index >= count) {
            throw new IllegalArgumentException("índice " + index + " fuera de 0.." + (count - 1));
        }
        return songs[index];
    }

    private void grow() {
        Song[] bigger = new Song[songs.length * 2];
        for (int i = 0; i < count; i++) {
            bigger[i] = songs[i];           // se copian referencias, no canciones
        }
        songs = bigger;                     // el arreglo viejo queda para el garbage collector
    }
}
music/ArraysDemo.javaDescargar music/ArraysDemo.java
package music;

// Un arreglo de objetos es un arreglo de referencias.
public class ArraysDemo {
    public static void main(String[] args) {
        int[] counts = new int[3];
        Song[] queue = new Song[3];
        IO.println("counts[0] = " + counts[0] + ", queue[0] = " + queue[0]);

        Song song = new Song("Cumbia del puente", "Los Rieleros", 187);
        queue[0] = song;
        queue[1] = song;                    // dos casillas, una sola canción
        IO.println("queue[0] == queue[1]: " + (queue[0] == queue[1]));

        Song[] copy = queue.clone();        // copia las referencias, no las canciones
        IO.println("copy == queue: " + (copy == queue) + ", copy[0] == queue[0]: " + (copy[0] == queue[0]));

        IO.println("queue[2].title() ...");
        IO.println(queue[2].title());       // queue[2] sigue en null
    }
}
music/Playlist.javaDescargar music/Playlist.java
package music;

import java.util.ArrayList;

/**
 * Una lista de reproducción. Guarda sus canciones en un ArrayList que nadie
 * más ve: los demás solo pueden usar los métodos de esta clase.
 */
public class Playlist {
    private final String name;
    private final ArrayList<Song> songs = new ArrayList<>();

    public Playlist(String name) {
        this.name = name;
    }

    public String name() {
        return name;
    }

    public void add(Song song) {
        if (song == null) {
            throw new IllegalArgumentException("no se puede agregar null");
        }
        songs.add(song);
    }

    public int size() {
        return songs.size();
    }

    /** La canción en la posición index, desde 0. */
    public Song get(int index) {
        return songs.get(index);
    }

    public boolean contains(Song song) {
        return songs.contains(song);
    }

    /** Duración total en segundos; 0 si está vacía. */
    public int totalSeconds() {
        int total = 0;
        for (Song s : songs) {
            total += s.seconds();
        }
        return total;
    }

    /** La canción más larga; si hay empate, la primera. La lista no puede estar vacía. */
    public Song longest() {
        if (songs.isEmpty()) {
            throw new IllegalArgumentException("la lista " + name + " está vacía");
        }
        Song best = songs.get(0);
        for (Song s : songs) {
            if (s.seconds() > best.seconds()) {
                best = s;
            }
        }
        return best;
    }

    /** Quita las canciones que duran menos de seconds. Devuelve cuántas quitó. */
    public int removeShorterThan(int seconds) {
        int removed = 0;
        // De atrás hacia adelante: al quitar una, las que faltan revisar no se mueven.
        for (int i = songs.size() - 1; i >= 0; i--) {
            if (songs.get(i).seconds() < seconds) {
                songs.remove(i);
                removed++;
            }
        }
        return removed;
    }

    /** Una copia de las canciones: cambiarla no cambia la lista. */
    public ArrayList<Song> songs() {
        return new ArrayList<>(songs);
    }
}
music/PlaylistDemo.javaDescargar music/PlaylistDemo.java
package music;

public class PlaylistDemo {
    public static void main(String[] args) {
        Playlist trip = new Playlist("Viaje");
        trip.add(new Song("Lima gris", "Grupo Garúa", 222));
        trip.add(new Song("Cumbia del puente", "Los Rieleros", 187));
        trip.add(new Song("Vals del Rímac", "Trío Malecón", 238));
        trip.add(new Song("Intro", "Varios", 41));

        IO.println(trip.name() + ": " + trip.size() + " canciones, " + trip.totalSeconds() + " s");
        IO.println("la más larga: " + trip.longest().title());
        Song wanted = new Song("Cumbia del puente", "Los Rieleros", 187);
        IO.println("¿tiene Cumbia del puente? " + trip.contains(wanted));
        IO.println("quitadas por cortas: " + trip.removeShorterThan(60) + ", quedan " + trip.size());

        ArrayPlaylist small = new ArrayPlaylist();
        for (int i = 1; i <= 5; i++) {
            small.add(new Song("Pista " + i, "Varios", 60 * i));
            IO.println("size " + small.size() + ", capacity " + small.capacity());
        }
    }
}
music/Song.javaDescargar music/Song.java
package music;

/** Una canción: un valor inmutable. La duración va en segundos y es positiva. */
public record Song(String title, String artist, int seconds) {

    public Song {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("la canción necesita un título");
        }
        if (seconds <= 0) {
            throw new IllegalArgumentException("duración no positiva: " + seconds);
        }
    }
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

library/LibraryTest.javaDescargar library/LibraryTest.java
package library;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class LibraryTest {
    private static final LocalDate SEPT_1 = LocalDate.of(2026, 9, 1);

    private final Book book = new Book("0001", "Cuentos del Rímac", "Rosa Quispe");
    private Library lib;
    private Copy copy1;
    private Copy copy2;
    private Member alice;

    @BeforeEach
    void setUp() {
        lib = new Library();
        copy1 = new Copy("B-001", book);
        copy2 = new Copy("B-002", book);
        lib.addCopy(copy1);
        lib.addCopy(copy2);
        alice = new Member("S-17", "Alice");
        lib.addMember(alice);
    }

    @Test
    void findCopyByCode() {
        assertSame(copy2, lib.findCopy("B-002"));
        assertNull(lib.findCopy("B-999"));
    }

    @Test
    void lendMarksCopyAndMember() {
        Loan loan = lib.lend(copy1, alice, SEPT_1);

        assertFalse(copy1.isAvailable());
        assertEquals(1, alice.activeLoanCount());
        assertEquals(1, lib.availableCopies(book));
        assertEquals(LocalDate.of(2026, 9, 15), loan.due());
    }

    @Test
    void cannotLendACopyThatIsOnLoan() {
        lib.lend(copy1, alice, SEPT_1);
        Member bob = new Member("S-18", "Bob");

        assertThrows(IllegalArgumentException.class, () -> lib.lend(copy1, bob, SEPT_1));
    }

    @Test
    void memberCannotExceedMaxLoans() {
        for (int i = 0; i < Member.MAX_LOANS; i++) {
            Copy c = new Copy("X-" + i, book);
            lib.addCopy(c);
            lib.lend(c, alice, SEPT_1);
        }

        assertFalse(alice.canBorrow());
        assertThrows(IllegalArgumentException.class, () -> lib.lend(copy1, alice, SEPT_1));
    }

    @Test
    void overdueOnlyAfterDueDate() {
        Loan loan = lib.lend(copy1, alice, SEPT_1);

        assertFalse(loan.isOverdue(LocalDate.of(2026, 9, 15)));
        assertTrue(loan.isOverdue(LocalDate.of(2026, 9, 16)));
        assertEquals(1, alice.overdueCount(LocalDate.of(2026, 9, 16)));
    }

    @Test
    void giveBackFreesCopyAndClearsOverdue() {
        Loan loan = lib.lend(copy1, alice, SEPT_1);

        lib.giveBack(loan);

        assertTrue(copy1.isAvailable());
        assertEquals(0, alice.activeLoanCount());
        assertFalse(loan.isOverdue(LocalDate.of(2026, 12, 31)));
    }

    @Test
    void cannotGiveBackTwice() {
        Loan loan = lib.lend(copy1, alice, SEPT_1);
        lib.giveBack(loan);

        assertThrows(IllegalArgumentException.class, () -> lib.giveBack(loan));
    }
}
music/ArrayPlaylistTest.javaDescargar music/ArrayPlaylistTest.java
package music;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class ArrayPlaylistTest {

    @Test
    void growsWhenFull() {
        ArrayPlaylist list = new ArrayPlaylist();
        Song first = new Song("Uno", "X", 60);
        list.add(first);
        list.add(new Song("Dos", "X", 60));
        list.add(new Song("Tres", "X", 60));

        assertEquals(3, list.size());
        assertEquals(4, list.capacity());
        assertSame(first, list.get(0));
    }

    @Test
    void getBeyondSizeIsRejectedEvenIfThereIsRoom() {
        ArrayPlaylist list = new ArrayPlaylist();
        list.add(new Song("Uno", "X", 60));

        // capacity es 2, pero la posición 1 no está en uso
        assertThrows(IllegalArgumentException.class, () -> list.get(1));
    }
}
music/PlaylistTest.javaDescargar music/PlaylistTest.java
package music;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class PlaylistTest {
    private final Song a = new Song("A", "X", 200);
    private final Song b = new Song("B", "X", 45);
    private final Song c = new Song("C", "Y", 300);
    private Playlist list;

    // Se ejecuta antes de cada prueba: cada una empieza con una lista nueva.
    @BeforeEach
    void setUp() {
        list = new Playlist("Prueba");
    }

    @Test
    void emptyPlaylistHasZeroDuration() {
        assertEquals(0, list.size());
        assertEquals(0, list.totalSeconds());
    }

    @Test
    void totalSecondsAddsAllSongs() {
        list.add(a);
        list.add(b);
        list.add(c);

        assertEquals(545, list.totalSeconds());
    }

    @Test
    void longestOfSeveral() {
        list.add(a);
        list.add(c);
        list.add(b);

        assertSame(c, list.longest());
    }

    @Test
    void longestKeepsTheFirstOnATie() {
        Song alsoTwoHundred = new Song("D", "Z", 200);
        list.add(a);
        list.add(alsoTwoHundred);

        assertSame(a, list.longest());
    }

    @Test
    void longestOfEmptyPlaylistIsRejected() {
        assertThrows(IllegalArgumentException.class, () -> list.longest());
    }

    @Test
    void sameSongCanBeAddedTwice() {
        list.add(a);
        list.add(a);

        assertEquals(2, list.size());
        assertEquals(400, list.totalSeconds());
    }

    @Test
    void containsComparesByValue() {
        list.add(a);

        assertTrue(list.contains(new Song("A", "X", 200)));
        assertFalse(list.contains(new Song("A", "X", 201)));
    }

    @Test
    void removeShorterThanRemovesConsecutiveSongs() {
        list.add(a);
        list.add(b);
        list.add(new Song("E", "Y", 30));
        list.add(c);

        int removed = list.removeShorterThan(60);

        assertEquals(2, removed);
        assertEquals(2, list.size());
        assertSame(a, list.get(0));
        assertSame(c, list.get(1));
    }

    @Test
    void songsReturnsACopy() {
        list.add(a);

        list.songs().clear();

        assertEquals(1, list.size());
    }

    @Test
    void rejectsNull() {
        assertThrows(IllegalArgumentException.class, () -> list.add(null));
    }
}
music/SongTest.javaDescargar music/SongTest.java
package music;

import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class SongTest {

    @Test
    void rejectsNonPositiveDuration() {
        assertThrows(IllegalArgumentException.class, () -> new Song("A", "X", 0));
    }

    @Test
    void rejectsBlankTitle() {
        assertThrows(IllegalArgumentException.class, () -> new Song(" ", "X", 60));
    }
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 4: arreglos, ArrayList y diseño con clases.
#   make         compila todo a build/ (build/music y build/library); con -g, para que los
#                mensajes de error y jdb nombren las variables locales
#   make test    corre las demostraciones y las pruebas JUnit (*Test.java)
#   make clean   borra build/
#
# Las fuentes se buscan en todas las subcarpetas: cada carpeta es un paquete
# (music/Playlist.java empieza con "package music;"). Las demostraciones se
# ejecutan por su nombre completo, con el paquete: music.PlaylistDemo.
JAVAC  = javac
JFLAGS = -Xlint:all -Werror -g
SRC    = $(sort $(shell find . -name '*.java' -not -path './build/*'))
DEMOS  = music.PlaylistDemo BoxingDemo library.LibraryDemo

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 "== music.ArraysDemo (termina con NullPointerException a propósito)"; ! java -cp build music.ArraysDemo
	@echo "== RemoveInLoop (termina con ConcurrentModificationException a propósito)"; ! java -cp build RemoveInLoop
	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