Semana 4 — Arreglos, ArrayList y diseño con clases
Objetivos
- Usar arreglos y
ArrayListpara 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:
- Eck, sec. 3.8 Introduction to Arrays, subsecciones 3.8.1 a 3.8.4 (repaso rápido desde C).
- Eck, sec. 7.1 Array Details, subsecciones 7.1.1 y 7.1.3.
- Eck, subsec. 7.2.4 Dynamic Arrays.
- Eck, sec. 7.3 ArrayList, subsecciones 7.3.1 a 7.3.3.
Lecturas complementarias:
- API de Java 25:
ArrayList(métodosadd,get,set,remove,size,contains,indexOf).
Temas:
- Arreglos en Java frente a C: conocen su longitud, se verifica el índice (
ArrayIndexOutOfBoundsException), se crean connewy viven en el heap. - Un arreglo de objetos guarda referencias:
new Song[10]crea dieznull, 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.
ArrayListhace 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>, noArrayList<int>. - Recorridos con
forclásico y con for-each. Ejemplo propio: unaPlaylistque encapsula suArrayList<Song>(Songes un record) y ofreceadd,totalSeconds,longestyremoveShorterThan;ArrayPlaylistmuestra a mano el arreglo que crece. containseindexOfcomparan conequals: con un record, por valor.- Pruebas:
@BeforeEach,assertSameyassertNull. - Error común: borrar elementos dentro de un for-each (
ConcurrentModificationException, o peor, un elemento que se salta sin aviso); ylist.remove(1)en unArrayList<Integer>, que borra la posición 1 y no el valor 1.
Sesión B — Diseñar con clases: responsabilidades, UML de clases
Lecturas:
- Eck, subsec. 5.3.4 Object-oriented Analysis and Design.
- Eck, subsec. 5.4.1 Designing the classes (un ejemplo de diseño, de cómo pasar de sustantivos y verbos a clases y métodos).
Lecturas complementarias (Eck no trata UML):
- K. Beck y W. Cunningham, A Laboratory For Teaching Object-Oriented Thinking (OOPSLA ‘89): las tarjetas CRC (clase, responsabilidades, colaboradores).
- Martin Fowler, UML as Sketch: UML como boceto para pensar y conversar, no como plano completo.
- Documentación de Mermaid, Class diagrams: la notación que usamos para dibujar diagramas de clases en texto.
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.
- Dado un enunciado corto (un dominio distinto del proyecto), hacer tarjetas CRC y un diagrama de clases en Mermaid.
- Implementar dos o tres de esas clases con
ArrayListencapsulado y sus invariantes. - Escribir las pruebas JUnit antes o junto con cada método: casos normales, lista vacía, elemento repetido, límites.
- 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
ArrayListencapsulado, y el diagrama de clases del laboratorio sirve como primer diseño del proyecto.
Preguntas de repaso
- ¿Qué contiene
new Song[3]justo después de crearse? ¿Ynew int[3]? - ¿Por qué
ArrayList<int>no compila? ¿Qué hace el compilador conlist.add(5)en unArrayList<Integer>? - En un
ArrayList<Integer>con[5, 1, 7], ¿qué queda después delist.remove(1)? ¿Cómo se borra el valor 1? - Dibujen el diagrama de clases de la biblioteca de la sesión B. ¿Qué multiplicidad tiene la asociación entre socio y préstamo?
- ¿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.
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));
}
}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);
}
}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");
}
}
}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;
}
}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);
}
}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());
}
}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();
}
}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);
}
}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
}
}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
}
}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);
}
}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());
}
}
}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.
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));
}
}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));
}
}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));
}
}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.
# 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