Apuntes · Capítulo 1Objetos y clases
De C a Java: objetos como estado y comportamiento
De C a Java: objetos como estado y comportamiento
En el curso de introducción a la programación aprendieron a resolver problemas con variables, condicionales, bucles, funciones y arreglos, en C o en C++. Todo eso sigue valiendo. Este curso agrega una forma distinta de organizar los programas: en lugar de datos por un lado y funciones que los manipulan por otro, vamos a construir objetos que guardan sus datos y saben qué hacer con ellos. Usaremos Java 25, la versión con soporte de largo plazo (LTS) más reciente.
Este primer capítulo tiene dos partes. En la primera pasamos de C a Java: qué
cambia al compilar, cómo se ejecuta un programa en la máquina virtual de Java,
qué son los tipos primitivos y las referencias, y por qué en Java no hay
punteros ni free. En la segunda respondemos la pregunta que da nombre al
curso: ¿por qué objetos? Tomamos un ejemplo pequeño, lo escribimos primero al
estilo C, con un struct y funciones, y después como una clase.
Lectura. Este capítulo acompaña a Eck, Introduction to Programming Using Java (versión 9), secciones 1.3 The Java Virtual Machine, 1.5 Objects and Object-oriented Programming, 2.1 a 2.3, la sección 2.6 Programming Environments (en especial 2.6.2 y 2.6.6, sobre
jshell) y las secciones 5.1 y 5.2; si quieren repasar arreglos, la 3.8. El libro cubre Java 17: los archivos fuente compactos convoid main()y la claseIOson de Java 25 y no aparecen ahí. El código del capítulo está encode/week-01y se prueba conmake testdentro del contenedor del curso (env/run.sh).
Ruta de lectura.
- Antes de clase (obligatorio, 15–20 min): El curso en una página, Compilar a bytecode, no a código máquina y El problema de los datos sueltos.
- En clase: en la sesión A, el resto de De C a Java: las tres herramientas, los archivos fuente compactos, los tipos primitivos, las referencias y el garbage collector; en la sesión B, Objetos como estado y comportamiento, de La idea: juntar estado y comportamiento a Mensajes, estado e identidad. En el laboratorio, El código de la semana y
make test.- Después de clase: Ejemplo resuelto: la tarjeta en
jshell, el Resumen y los Ejercicios. Para ampliar, las secciones de Eck de la nota Lectura.
El curso en una página
El curso tiene cuatro partes: objetos y clases (semanas 1 a 4), herencia, polimorfismo e interfaces (5 a 8), programar con tipos: excepciones, genéricos, colecciones, lambdas y streams (9 a 12), y diseño: principios, patrones y pruebas (13 a 16). El calendario completo, las lecturas de cada semana y las políticas están en el programa del curso.
Todo el semestre trabajarán en un solo proyecto que crece: Bodega, el sistema de inventario y ventas de una tienda pequeña. Empieza con unos programas cortos y una primera clase (P0) y termina como una aplicación de línea de comandos con persistencia en archivos y patrones de diseño (P5). Cada entrega debe pasar sus pruebas y las de las entregas anteriores, así que el código que escriban en la semana 2 lo van a seguir usando en la semana 16. Eso obliga a escribirlo bien.
Además del proyecto, hay un laboratorio cada semana, un examen parcial en la semana 8 y un examen final al cierre del curso.
El entorno es el mismo para todos: el JDK 25 y JUnit dentro de un contenedor Docker que funciona igual en Linux, Windows (WSL2) y macOS. Lo instalan en el laboratorio de esta semana.
De C a Java
Java nació en Sun Microsystems, en un equipo dirigido por James Gosling, y se publicó en 1995. La idea que lo hizo popular fue escribir una vez, ejecutar en cualquier parte: el mismo programa compilado tenía que correr sin cambios en máquinas con procesadores y sistemas operativos distintos. Hoy Java lo mantiene Oracle junto con la comunidad OpenJDK; sale una versión nueva cada seis meses y, cada dos años, una LTS. Java 25 (septiembre de 2025) es la LTS actual.

La sintaxis les va a resultar familiar: Java tomó de C las llaves, el punto y
coma, if, while, for, los operadores y casi todos los tipos numéricos.
Lo que cambia es lo que pasa por debajo.
Compilar a bytecode, no a código máquina
En C, gcc traduce el archivo fuente a código máquina de un procesador
concreto. El ejecutable que sale de una laptop con procesador x86-64 no corre
en una con procesador ARM: hay que volver a compilar en cada plataforma.
Java agrega un nivel intermedio. El compilador javac no produce código
máquina sino bytecode: instrucciones para una computadora imaginaria, la
máquina virtual de Java (Java Virtual Machine, JVM). El bytecode se guarda
en archivos .class. Cada plataforma tiene su propia JVM, que es un programa
común y corriente compilado para ese procesador, y esa JVM es la que ejecuta
el bytecode. Por eso el mismo .class corre en todas.


¿Y no es lento interpretar instrucciones de una máquina imaginaria? Al principio lo era. Las JVM modernas, como HotSpot, la del JDK que usamos, empiezan interpretando el bytecode, pero van midiendo qué métodos se ejecutan mucho y los compilan a código máquina mientras el programa corre. Ese compilador se llama JIT (just-in-time). Un programa que corre varios segundos termina ejecutando casi todo su tiempo en código máquina optimizado para el procesador exacto donde está.
En Java. La última línea de
java -versionlo dice:$ java -version openjdk version "25.0.5" 2026-10-20 LTS OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS) OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing)Mixed mode significa justamente eso: intérprete más JIT. Temurin es el nombre de la distribución del JDK (el código es el de OpenJDK).
Se puede mirar el bytecode. javap -c desensambla un .class, igual que
objdump -d desensambla un ejecutable de C:
$ javac -d build Hello.java
$ javap -c build/Hello.class
Compiled from "Hello.java"
final class Hello {
Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
void main();
Code:
0: ldc #7 // String Hola, Java
2: invokestatic #9 // Method java/lang/IO.println:(Ljava/lang/Object;)V
5: return
}
No hace falta entender cada instrucción. Fíjense en dos cosas: ldc carga la
cadena "Hola, Java" e invokestatic llama a IO.println. Y aunque nuestro
archivo no decía class en ningún lado, el compilador creó una clase llamada
Hello. En Java todo el código vive dentro de una clase; la siguiente
sección explica de dónde salió esta.
Tres herramientas: javac, java y jshell
El JDK (Java Development Kit) trae muchas herramientas. Esta semana usamos tres:
| Herramienta | Qué hace | Equivalente en C |
|---|---|---|
javac |
Compila .java a .class (bytecode) |
gcc -c |
java |
Arranca la JVM y ejecuta una clase | ejecutar ./programa |
jshell |
Evalúa código Java línea por línea | no hay; se parece al intérprete de Python |
El archivo más corto que podemos escribir es
Hello.java:
void main() {
IO.println("Hola, Java");
}
Hay dos maneras de ejecutarlo. La de dos pasos, como en C, compila primero y
ejecuta después; -d build le dice a javac dónde dejar los .class, y
-cp build (class path) le dice a java dónde buscarlos:
$ javac -d build Hello.java
$ ls build
Hello.class
$ java -cp build Hello
Hola, Java
Fíjense en que a java se le da el nombre de la clase (Hello), no el del
archivo. La otra manera es pasarle a java el archivo fuente directamente: la
JVM lo compila en memoria y lo ejecuta, sin dejar ningún .class. Es cómoda
para programas de un archivo:
$ java Hello.java
Hola, Java
jshell sirve para probar una idea sin escribir un programa completo. Cada
línea se evalúa al momento y jshell muestra el valor que quedó en la
variable:
jshell> int x = 7 / 2
x ==> 3
jshell> double y = 7 / 2.0
y ==> 3.5
jshell> String s = "Lima"
s ==> "Lima"
jshell> int len = s.length()
len ==> 4
Igual que en C, la división entre dos enteros descarta los decimales. Para
salir de jshell se escribe /exit.
Archivos fuente compactos
En C, un programa empieza en main. En Java también, pero hasta hace poco el
programa más corto necesitaba toda esta ceremonia:
public class Hello {
public static void main(String[] args) {
System.out.println("Hola, Java");
}
}
public, class, static y String[] args son ideas que este curso explica
en las próximas semanas, y obligar a escribirlas el primer día confunde más
de lo que ayuda. Java 25 agregó los archivos fuente compactos (compact
source files, JEP 512): un archivo puede
contener directamente métodos y campos, sin declarar una clase, y main puede
ser simplemente void main(). El compilador envuelve todo en una clase
implícita con el nombre del archivo; es la final class Hello que vimos en el
bytecode. Además, IO.println imprime en la salida estándar (como printf
con \n al final) e IO.readln lee una línea del teclado, y en un archivo
compacto están disponibles sin import todas las clases de uso común de la
biblioteca estándar.
Idea clave. Un archivo compacto es Java normal con menos ceremonia. Lo usaremos en las semanas 1 y 2 para programas cortos y demostraciones. Las clases de verdad, como
TransitCardmás abajo, van en su propio archivo conclass, y desde la semana 3 todo el código va así.
Tipos primitivos
Java tiene ocho tipos primitivos, casi los mismos de C, con una diferencia
importante: su tamaño está fijado por el lenguaje y no depende de la
máquina. En C, un int puede tener 16, 32 o 64 bits según el compilador; en
Java siempre tiene 32.
| Tipo | Tamaño | Valores |
|---|---|---|
byte |
8 bits | −128 a 127 |
short |
16 bits | −32 768 a 32 767 |
int |
32 bits | unos ±2 100 millones |
long |
64 bits | unos ±9,2 × 10¹⁸ |
float |
32 bits | punto flotante IEEE 754, simple precisión |
double |
64 bits | punto flotante IEEE 754, doble precisión |
char |
16 bits | una unidad de código UTF-16 |
boolean |
— | true o false |
No hay unsigned. Y boolean es un tipo aparte: no es un número, así que una
condición tiene que ser de tipo boolean.
Un char no es exactamente “un carácter”. Es una unidad de código UTF-16
de 16 bits. Las letras del español, con ñ y tildes, caben en un char. Pero
Unicode tiene más de un millón de puntos de código (code points), y los que no
caben en 16 bits, como la mayoría de los emojis, ocupan dos char. Además, lo que en
pantalla se ve como una sola letra puede estar formado por varios puntos de
código: una e seguida de un acento combinante, por ejemplo. Para el texto de
este curso un char por letra basta; conviene saber que no es la regla general.
Error común. Escribir una condición al estilo C:
int n = 5; if (n) { IO.println("cinco"); }$ javac -Xlint:all Check.java Check.java:3: error: incompatible types: int cannot be converted to boolean if (n) { ^ 1 errorEn C,
if (n)significa “sinno es cero”. En Java hay que decirlo:if (n != 0). Es una de las reglas que evita errores comoif (x = 0), que en C compila y casi nunca es lo que se quería.
FromC.java muestra el integer
overflow: cuando un int pasa de su máximo, da la vuelta (wraps around) al
mínimo, y siempre al mismo valor, porque el tamaño es fijo. En C, el overflow de un entero con signo es
comportamiento indefinido (undefined behavior): el compilador puede asumir que
nunca pasa.
int n = 2_147_483_647; // int siempre tiene 32 bits
long big = n + 1L; // long siempre tiene 64 bits
IO.println("n + 1 como int: " + (n + 1));
IO.println("n + 1 como long: " + big);
$ java -cp build FromC
n + 1 como int: -2147483648
n + 1 como long: 2147483648
...
(Los guiones bajos dentro de un número solo sirven para leerlo mejor.)
Referencias: String y arreglos
Todo lo que no es un tipo primitivo es un objeto, y a los objetos se llega
a través de referencias (references). Una variable int guarda un número.
Una variable String no guarda las letras: guarda una referencia, algo así como
una flecha, a un objeto String que vive en otra parte de la memoria, el
heap. Lo mismo pasa con los arreglos y con cualquier clase que escribamos.
Si vienen de C, una referencia se parece a un puntero, con tres diferencias que
importan mucho: no se puede hacer aritmética con ella (no existe p + 1), no
se puede convertir en un número ni un número en referencia, y no puede apuntar
a cualquier lugar: solo a un objeto del tipo correcto, o a ninguno. La semana 2
estudiamos las referencias en detalle.
Dos tipos de referencia aparecen en todo programa:
Stringes una clase de la biblioteca estándar. UnStringno es unchar*terminado en'\0': es un objeto que sabe su longitud y ofrece métodos, comolength(),toUpperCase()ocontains(...). Y no se puede modificar:toUpperCase()no cambia el original, devuelve otroString.- Los arreglos son objetos que conocen su longitud, en el campo
length. Se crean connew, y sus elementos empiezan en cero (ofalse, onull), nunca con basura.
int[] marks = new int[4]; // los elementos empiezan en 0
marks[0] = 15;
marks[3] = 18;
int sum = 0;
for (int i = 0; i < marks.length; i++) {
sum += marks[i];
}
En C, leer marks[4] en un arreglo de 4 elementos es comportamiento
indefinido: el programa lee lo que haya en esa memoria, y a veces parece
funcionar. En Java cada acceso se verifica:
jshell> int[] a = new int[3]
a ==> int[3] { 0, 0, 0 }
jshell> int n = a.length
n ==> 3
jshell> a[3] = 1
| Exception java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
| at (#7:1)
El programa se detiene con una excepción que dice exactamente qué pasó: el índice 3 está fuera de un arreglo de longitud 3. Las excepciones son el tema de la semana 9; por ahora, lean el mensaje: casi siempre dice dónde está el error.
Sin punteros ni free: el garbage collector
En C, la memoria que se pide con malloc se devuelve con free. Si se olvida
el free, el programa pierde memoria (un memory leak); si se hace
free dos veces, o se usa un puntero después del free, el programa puede
hacer cualquier cosa.
En Java se pide memoria con new, y no hay free. La JVM tiene un
garbage collector que, de vez en cuando, busca los
objetos a los que ya no llega ninguna referencia del programa y recupera su
memoria. Un objeto al que nadie puede llegar no le sirve a nadie, así que
liberarlo es seguro. Con eso desaparecen de un golpe los memory leaks por
olvidar un free, los free dobles y los dangling pointers (punteros a
memoria ya liberada). Lo que el garbage collector no resuelve es un objeto que
sigue alcanzable aunque el programa ya no lo necesite: ese no se libera nunca.
Es la forma que toman los memory leaks en Java, y la semana 2 vuelve sobre ella.
En Java. El garbage collector no es gratis: trabaja mientras el programa corre y consume algo de CPU. Los garbage collectors modernos de HotSpot, como G1 (el que la JVM elige en casi todas las computadoras actuales) y ZGC, hacen casi todo su trabajo en paralelo con el programa, con pausas muy cortas. Para este curso basta con saber que existe y que no hay que liberar nada a mano.
Idea clave. De C a Java: se compila a bytecode que ejecuta una JVM (con intérprete y JIT); los tipos primitivos tienen tamaño fijo; todo lo demás son objetos a los que se llega por referencias, sin aritmética de punteros; los arreglos conocen su longitud y verifican los índices; y la memoria la libera el garbage collector.
Objetos como estado y comportamiento
El problema de los datos sueltos
Pensemos en la tarjeta del transporte público de una ciudad: tiene un código y
un saldo, se recarga y se usa para pagar el pasaje. En C la escribiríamos con
un struct para los datos y funciones aparte que reciben un puntero al
struct (transit_card.c):
struct card {
char id[8];
int balance; /* saldo en céntimos */
};
void card_recharge(struct card *c, int amount) {
c->balance += amount;
}
int card_pay(struct card *c, int fare) {
if (c->balance < fare)
return 0; /* no alcanza el saldo */
c->balance -= fare;
return 1;
}
Guardamos el saldo en céntimos, como un entero, para no tener problemas de redondeo con el punto flotante: S/ 10.00 son 1000 céntimos.
Este código funciona, y es como se escriben muchos programas grandes en C. Pero
tiene un problema que crece con el programa. Las reglas de la tarjeta (que el
saldo no sea negativo, que un pasaje solo se cobre si alcanza) viven en
card_pay, pero nada obliga a pasar por ahí. Cualquier función de
cualquier archivo puede escribir directamente en el campo. El main de
transit_card.c lo hace:
int main(void) {
struct card c = { "A-001", 0 };
card_recharge(&c, 1000);
card_pay(&c, 250);
printf("%s: %d céntimos\n", c.id, c.balance);
c.balance = -500; /* nada lo impide */
printf("%s: %d céntimos\n", c.id, c.balance);
return 0;
}
El contenedor del curso trae gcc, así que make también compila la versión
en C:
$ make
...
gcc -Wall -Wextra -Werror -O1 -o build/transit_card transit_card.c
$ ./build/transit_card
A-001: 750 céntimos
A-001: -500 céntimos
El compilador no se queja y el programa tampoco: la tarjeta quedó con saldo negativo.
En un programa de diez líneas eso se ve. En uno de cincuenta mil, con diez
personas trabajando, alguien lo va a hacer, por error o por apuro, y cuando el
saldo aparezca negativo habrá que revisar todas las funciones que tocan
balance para saber cuál fue. Además, nada dice qué funciones “pertenecen” a
la tarjeta: card_recharge y card_pay están juntas porque las pusimos
juntas y les pusimos el mismo prefijo, no porque el lenguaje las relacione con
struct card.
El mismo problema aparece en Java si escribimos al estilo C.
StructStyle.java usa una clase solo como
registro de datos, con funciones aparte:
class CardData {
String id;
int balance; // saldo en céntimos
}
void recharge(CardData card, int amount) {
card.balance += amount;
}
$ java -cp build StructStyle
A-001: 750 céntimos
A-001: -500 céntimos
La segunda línea es la tarjeta rota: el main le asignó −500 directamente.
Usar Java no basta; hay que organizar el programa de otra forma.
La idea: juntar estado y comportamiento
Un objeto (object) junta dos cosas:
- Estado (state): los datos que recuerda. La tarjeta recuerda su código y su saldo. En Java el estado se guarda en campos (fields), también llamados atributos.
- Comportamiento (behavior): lo que sabe hacer. La tarjeta sabe recargarse, cobrar un pasaje y decir su saldo. En Java el comportamiento se escribe en métodos (methods).
El resto del programa no toca el estado: le envía un mensaje al objeto
(“recárgate con 1000”, “cobra 250”) y el objeto responde ejecutando el método
que corresponde. Es lo que en C hacíamos con card_pay(&c, 250), escrito al
revés: primero a quién, después qué: alice.pay(250).
Una clase (class) es la descripción de un tipo de objeto: qué campos tiene cada objeto y qué métodos entiende. Cada objeto creado a partir de la clase es una instancia (instance) de ella. Si la clase es el plano, los objetos son las tarjetas concretas que andan en los bolsillos, cada una con su propio saldo.
Una primera clase
public class TransitCard {
private final String id;
private int balance; // saldo en céntimos
/** Crea una tarjeta con el código dado y saldo cero. */
public TransitCard(String id) {
this.id = id;
this.balance = 0;
}
/** Agrega saldo a la tarjeta. */
public void recharge(int amount) {
balance += amount;
}
/** Cobra un pasaje si alcanza el saldo; devuelve si se pudo cobrar. */
public boolean pay(int fare) {
if (balance < fare) {
return false;
}
balance -= fare;
return true;
}
public String id() {
return id;
}
public int balance() {
return balance;
}
}
Por partes:
- Los campos
idybalanceson el estado. Cada objetoTransitCardtiene los suyos.privatesignifica que solo el código de esta clase puede usarlos;finaldice queidno cambia después de crear la tarjeta. Los veremos con calma en la semana 3, al hablar de encapsulamiento (encapsulation). - El constructor (constructor) es el bloque que se llama igual que la clase
y no tiene tipo de retorno. Se ejecuta una vez, al crear el objeto, y deja
sus campos con valores válidos.
this.ides el campo del objeto que se está creando;id, a secas, es el parámetro. - Los métodos
recharge,pay,idybalanceson el comportamiento. Dentro de un método,balancese refiere al campo del objeto que recibió el mensaje. Por esopayno necesita un parámetrostruct card *c: el objeto ya sabe cuál es. - Los métodos
id()ybalance()solo devuelven un campo. Así se puede leer el saldo sin poder cambiarlo.
Idea clave. Las reglas de la tarjeta están ahora en un solo lugar: la clase. Como
balanceesprivate, la única forma de cambiar el saldo es enviarrechargeopay. Si un día aparece un saldo negativo, el error está en esos métodos y en ningún otro sitio.
new, paso a paso
Un objeto se crea con new:
TransitCard alice = new TransitCard("A-001");
Esa línea hace tres cosas, en orden:
newreserva el objeto en el heap, con espacio para todos sus campos, y los deja en su valor por defecto:0para los números,falseparabooleanynull(ninguna referencia) para las referencias.- Se ejecuta el constructor, que inicializa los campos:
idpasa a ser"A-001"ybalance, 0. newdevuelve una referencia al objeto, y esa referencia se guarda en la variablealice.


Después, alice.recharge(1000) es un mensaje: el objeto al que apunta alice
ejecuta recharge y su balance pasa a 1000.
Error común. Declarar una variable no crea un objeto.
TransitCard alice;solo reserva una variable capaz de guardar una referencia; el objeto aparece recién connew. La semana 2 vemos qué pasa si se usa una variable que no apunta a ningún objeto.
Mensajes, estado e identidad
CardDemo.java crea dos tarjetas y les envía
mensajes:
TransitCard alice = new TransitCard("A-001");
TransitCard bob = new TransitCard("A-002");
alice.recharge(1000);
boolean paid = alice.pay(250);
IO.println(alice.id() + ": pagó " + paid + ", saldo " + alice.balance());
// bob tiene su propio estado: el pasaje de alice no lo toca.
paid = bob.pay(250);
IO.println(bob.id() + ": pagó " + paid + ", saldo " + bob.balance());
// Identidad: dos new crean dos objetos distintos.
TransitCard same = alice;
IO.println("alice == bob: " + (alice == bob));
IO.println("alice == same: " + (alice == same));
$ java -cp build CardDemo
A-001: pagó true, saldo 750
A-002: pagó false, saldo 0
alice == bob: false
alice == same: true
Tres ideas salen de esta salida:
- Cada objeto tiene su propio estado. El mismo mensaje,
pay(250), tiene resultados distintos: la tarjeta de Alice tenía saldo y la de Bob no. El método es uno solo, escrito una vez en la clase; lo que cambia es el estado del objeto que lo recibe. - El estado cambia con los mensajes. Después de
recharge(1000)ypay(250), el saldo dealicees 750. Un objeto recuerda lo que le pasó. - Cada objeto tiene identidad (identity). Cada
newcrea un objeto distinto, aunque sea de la misma clase y aunque sus campos tuvieran los mismos valores. Con referencias,==pregunta si las dos flechas llegan al mismo objeto:alice == bobesfalse;alice == sameestrue, porquesame = alicecopió la flecha, no el objeto.
La diferencia entre que dos objetos sean el mismo (identidad) y que se parezcan (igualdad) es uno de los temas de la semana 2.
Ejemplo resuelto: la tarjeta en jshell
jshell puede cargar una clase con /open y probarla de forma interactiva.
Dentro de code/week-01:
jshell> /open TransitCard.java
jshell> TransitCard c = new TransitCard("A-001")
c ==> TransitCard@31221be2
jshell> c.recharge(500)
jshell> int b = c.balance()
b ==> 500
jshell> c.balance = 99
| Error:
| balance has private access in TransitCard
| c.balance = 99
| ^-------^
Línea por línea:
/opencompila la clase dentro dejshell.- Al crear la tarjeta,
jshellmuestra algo comoTransitCard@31221be2: el nombre de la clase y un número que identifica al objeto. Ese número cambia en cada ejecución; en la semana 5 veremos cómo hacer que un objeto se imprima de forma más útil. c.recharge(500)no devuelve nada (void), así quejshellno muestra ningún valor.c.balance()con paréntesis es el método, y devuelve 500.c.balance = 99, sin paréntesis, intenta tocar el campo directamente, y el compilador lo rechaza porque esprivate. Es exactamente la línea que en la versión al estilo C rompía la tarjeta. Ahora el error aparece al compilar, antes de que el programa llegue a ejecutarse.
El código de la semana y make test
Cada semana tiene su carpeta code/week-NN/ con un Makefile. Dentro del
contenedor del curso:
$ cd code/week-01
$ make test
mkdir -p build
javac -Xlint:all -Werror -cp /opt/junit/junit.jar -d build CardDemo.java FromC.java Hello.java StructStyle.java TransitCard.java TransitCardTest.java
touch build/.stamp
mkdir -p build
gcc -Wall -Wextra -Werror -O1 -o build/transit_card transit_card.c
== transit_card (C)
A-001: 750 céntimos
A-001: -500 céntimos
== Hello
Hola, Java
...
╷
├─ JUnit Platform Suite ✔
├─ JUnit Jupiter ✔
│ └─ TransitCardTest ✔
│ ├─ payWithEnoughBalance() ✔
│ ├─ newCardStartsEmpty() ✔
│ └─ payWithoutBalanceChangesNothing() ✔
└─ JUnit Vintage ✔
...
[ 3 tests successful ]
[ 0 tests failed ]
make compila todo a build/: el código Java con javac -Xlint:all -Werror
y la versión en C con gcc -Wall -Wextra -Werror. Cualquier advertencia del
compilador detiene la compilación, así que el código del curso compila sin
warnings. make test ejecuta las demostraciones y luego las pruebas de
TransitCardTest.java con JUnit, el
framework de unit tests que usaremos todo el semestre.
Por ahora basta con ver que corre y que las tres pruebas pasan; en la semana 3
aprenderán a escribirlas. make clean borra build/.
Resumen
- Java compila a bytecode (
.class), que la JVM de cada plataforma ejecuta interpretándolo y compilando con el JIT lo que más se usa. El mismo.classcorre en todas partes. javaccompila,javaejecuta (una clase o directamente un archivo fuente) yjshellevalúa código línea por línea.- Un archivo fuente compacto (Java 25) contiene métodos sin declarar una
clase;
void main()es su punto de entrada eIO.printlnimprime. - Los tipos primitivos tienen tamaño fijo;
booleanno es un número. Todo lo demás son objetos, a los que se llega por referencias, sin aritmética de punteros. Stringes un objeto inmutable con métodos; los arreglos conocen sulengthy verifican cada índice.- No hay
free: el garbage collector libera los objetos a los que ya no llega ninguna referencia. Los que siguen alcanzables no se liberan, aunque ya no sirvan. - Datos sueltos más funciones aparte dejan las reglas sin dueño: cualquier parte del programa puede romper los datos.
- Un objeto junta estado (campos) y comportamiento (métodos); los demás le envían mensajes. Una clase describe sus objetos; cada uno es una instancia.
newreserva el objeto, ejecuta el constructor y devuelve una referencia. Cada objeto tiene su propio estado y su propia identidad.
Ejercicios
-
Expliquen con sus palabras por qué el mismo
Hello.classcorre en una laptop con Linux y en otra con macOS y procesador ARM, mientras que un ejecutable compilado congccno. ¿Qué programa sí tiene que estar compilado para cada plataforma? -
Compilen
Hello.javaconjavac -d buildy desensámblenlo conjavap -c build/Hello.class. Cambien el texto que se imprime y agreguen una segunda línea conIO.println. ¿Qué instrucciones nuevas aparecen en el bytecode? -
En
jshell, evalúenint big = 2_147_483_647 + 1,long ok = 2_147_483_647L + 1,int r = 7 % 2,int c = 'a' + 1ychar d = (char) ('a' + 1). Antes de cada una, escriban qué esperan ver. Expliquen las que los sorprendieron. -
En
jshell, creenint[] a = new int[3]y pruebena[-1] = 5. Luego escriban en el contenedor un programa en C equivalente,bounds.c, que declareint a[3] = {0, 0, 0};, hagaa[-1] = 5;e imprima los tres elementos. Compílenlo congcc -Wall -Wextra -o bounds bounds.cy ejecútenlo varias veces. ¿Qué dice el compilador? ¿Qué pasa al ejecutar? ¿Cuál de los dos comportamientos prefieren cuando están buscando un error, y por qué? -
Escriban un archivo compacto
Initials.javaque guarde un nombre completo en unStringe imprima sus iniciales, usandolength()ycharAt(i). Ejecútenlo de las dos maneras: conjava Initials.javay compilando conjavacprimero. -
Sin ejecutarlo, digan qué imprime este fragmento. Después compruébenlo en
jshell(con/open TransitCard.java) y, si fallaron, expliquen en qué se equivocaron:TransitCard x = new TransitCard("B-1"); TransitCard y = new TransitCard("B-1"); TransitCard z = x; z.recharge(500); IO.println(x.balance() + " " + y.balance()); IO.println((x == y) + " " + (x == z)); -
Agreguen a
TransitCardun campo que cuente cuántos pasajes se pagaron con éxito y un métodotrips()que lo devuelva. ¿Qué métodos tienen que cambiar? ModifiquenCardDemopara mostrarlo y verifiquen quemake testsigue pasando. -
En
StructStyle.java, cualquier parte del programa puede poner un saldo negativo. Con la claseTransitCardque tienen, ¿todavía hay alguna forma de dejar el saldo negativo usando solo sus métodos públicos? Si la hay, muéstrenla con un ejemplo. (No la arreglen todavía: es el tema de la semana 3.) -
Elijan un objeto de la vida real que no sea una tarjeta (un ascensor, un semáforo, una cuenta de un servicio de streaming…). Escriban qué campos tendría su estado y qué mensajes entendería. Luego escríbanlo dos veces: al estilo C (un registro con funciones aparte) y como clase con constructor y métodos.
-
Supongan que en
CardDemoagregan, al final, la líneaalice = new TransitCard("A-003");. ¿Qué pasa con el objeto de la tarjetaA-001? ¿Puede el programa volver a usarlo? ¿Quién libera su memoria y cuándo? -
Lean la sección 1.5 de Eck. Él describe un objeto como un módulo que guarda datos y las subrutinas que los manipulan. Comparen esa descripción con la idea de “estado más comportamiento” de este capítulo. ¿Qué gana el programa al juntar las dos cosas?
-
En
TransitCardTest.java, cambien el valor esperado de una prueba para que sea incorrecto (por ejemplo, 700 en lugar de 750) y ejecutenmake test. Lean el mensaje de JUnit: ¿qué prueba falló, qué esperaba y qué obtuvo? Después devuelvan el valor correcto.