Ir al contenido principal

Lección 2 · Qué es Java

Qué es Java

La respuesta corta es que Java es un lenguaje de programación, pero esa respuesta se queda corta, y la propia documentación de Sun lo admite: Java no es solo un lenguaje. Es las cuatro cosas a la vez:

  • Un lenguaje de programación, con su sintaxis y sus reglas.
  • Una plataforma de desarrollo: el conjunto de herramientas para escribir, compilar y empaquetar programas.
  • Un entorno de ejecución: la máquina virtual que hace andar esos programas.
  • Un conjunto de bibliotecas para resolver problemas concretos —redes, archivos, bases de datos, ventanas—, que se llama API (Application Programming Interface).

Es un lenguaje derivado de C++, del que toma la sintaxis, con dos diferencias que explican casi todo lo demás:

  • Se le sacó lo peligroso. Java no tiene herencia múltiple, ni punteros, ni la sentencia goto. Son las tres cosas que en C++ producen la mayor cantidad de errores difíciles de encontrar. Y agregó dos cosas que C++ no tenía: gestión automática de memoria —uno no libera la memoria a mano— y soporte del lenguaje para programas multihilo.
  • No se compila para una máquina: se compila para una máquina virtual. El compilador de Java no genera un ejecutable para Windows ni para Linux: genera un archivo en un código intermedio llamado bytecode, que después ejecuta la JVM (Java Virtual Machine) que está instalada en cada sistema. De ahí sale su característica más conocida: el mismo programa compilado corre en cualquier sistema sin volver a compilarlo.

Esa última decisión tiene una consecuencia que casi nadie tiene presente y que explica un montón de cosas de la sintaxis: como los tipos de datos tienen que comportarse igual en todas las máquinas, en Java un int ocupa siempre lo mismo —32 bits— en cualquier computadora. En C un int puede ser de 16, 32 o 64 bits según dónde se compile, y eso obliga a escribir programas distintos para cada plataforma.

Historia del lenguaje

Vale la pena conocerla porque explica para qué se pensó Java y por qué terminó siendo lo que es. La historia tiene tres etapas bien marcadas.

Green: el proyecto para electrodomésticos (1991)

En 1991 Sun Microsystems patrocinó un proyecto interno de investigación llamado Green. El objetivo era diseñar un lenguaje nuevo destinado a electrodomésticos: la idea era que el mismo programa pudiera correr en una tostadora, un televisor o un decodificador, aparatos con procesadores distintos y sin un sistema operativo común.

El lenguaje lo creó James Gosling, partiendo de C++. Lo llamó Oak —«roble»— por un árbol que tenía a la vista desde la ventana de su oficina. Cuando fueron a registrarlo descubrieron que ya existía un lenguaje con ese nombre, así que hubo que cambiarlo: la versión más repetida dice que eligieron Java —una variedad de café— mientras tomaban café en una cafetería del barrio.

La web cambia el destino del lenguaje (1993-1995)

El proyecto Green no tuvo el éxito comercial que se esperaba con los electrodomésticos, pero en 1993 llegó la web y Sun vio otra oportunidad: usar Java para agregar contenido dinámico a las páginas —interactividad, animaciones—, algo que el HTML de entonces no permitía. Java se anunció formalmente en mayo de 1995 y la atención de la comunidad de negocios fue inmediata, por el interés fenomenal que despertaba la World Wide Web.

De esa época viene una decisión que todavía se discute: el lenguaje nació pensado para ejecutarse en lugares desconocidos, y por eso trae la seguridad como parte de su diseño. Un programa que llega por la red no puede hacer cualquier cosa en la máquina donde corre.

De Sun a Oracle, y Java hoy

En 2009 Oracle adquirió Sun Microsystems, así que Java pasó a manos de Oracle. En la conferencia JavaOne de 2010 la empresa dio unos números que muestran la escala que había alcanzado: el 97 % de las computadoras de escritorio, tres mil millones de dispositivos portátiles y 80 millones de televisores ejecutaban Java. Los apuntes agregan que por entonces había unos 12 millones de desarrolladores de Java, contra 4,5 millones en 2005.

Hoy Java se sigue usando para desarrollar aplicaciones empresariales de gran escala, para la funcionalidad del lado del servidor de los sitios web, y en dispositivos de todo tipo: teléfonos, televisores, terminales de punto de venta. También es el lenguaje de las aplicaciones de Android. Los números exactos del párrafo anterior son de aquel momento y quedaron como foto de una época: lo que no cambió es el lugar de Java entre los lenguajes más usados del mundo, sobre todo en el desarrollo de sistemas grandes.

Las diez propiedades

En una de las primeras publicaciones que describen el lenguaje (Zukowski, 1997), Sun lo presenta con una lista de adjetivos que conviene conocer uno por uno, porque cada uno es una decisión de diseño y no una virtud publicitaria.

Propiedad Qué significa
Sencillo Más fácil que C++, que era el lenguaje más popular cuando apareció Java. Se tomó el núcleo de C++ y se dejaron afuera las partes que lo hacían complicado, y la sintaxis quedó parecida al inglés, lo que facilita leer el código.
Orientado a objetos Programar en Java es crear, manipular y construir objetos. El lenguaje solo trabaja con objetos, con la única excepción de unos pocos tipos de datos primitivos. La herramienta con la que se hace eso es la clase, que encapsula los datos y los métodos que los manejan.
Distribuido La computación distribuida es la que reparte el trabajo entre varias computadoras conectadas en red. Java trae una biblioteca de clases para eso: permite abrir sockets y comunicarse con servidores y clientes remotos sin herramientas extra.
Compilado e interpretado Es asombroso a la vez. El compilador genera bytecode para la JVM en lugar de código nativo de la máquina, y ese bytecode después se interpreta. Es la combinación de los dos modelos, y es la clave de la portabilidad.
De arquitectura neutral y portable Consecuencia de lo anterior: el bytecode no depende de ninguna arquitectura, así que un programa corre en cualquier sistema que tenga una JVM. Es la característica más notable del lenguaje.
Multiplataforma Otra consecuencia, pero del lado de quien programa: no hay que preocuparse por el tamaño de los tipos de datos ni por las capacidades aritméticas de cada procesador. El eslogan con el que Sun lo vendía fue «Write Once, Run Anywhere» —escribí una vez, ejecutá donde sea—.
Multihilo Soporta varios hilos de ejecución a la vez, cada uno con su tarea, y soporta sincronizarlos. Permite que un hilo atienda la comunicación, otro la interacción con el usuario, otro una animación y otro haga cálculos, todo en el mismo programa. Se desarrolla en la lección 6.
Dinámico En cualquier momento de la ejecución Java puede ampliar sus capacidades enlazando clases que estén en la máquina, en un servidor remoto o en cualquier sitio de la red. Eso es imposible en C++, donde las clases se enlazan al compilar y después no se tocan.
Robusto Está diseñado para que los programas aguanten. No elimina la necesidad de probar el software ni evita que uno se equivoque, pero al ser fuertemente tipado se asegura de que cada variable se use para lo que fue declarada y no se escapen errores de conversión. Además exige declarar los métodos —cosa que C no hace—, elimina los punteros y trae manejo de excepciones y recolección de basura.
Seguro Es el punto que más importa cuando el código llega por Internet. El lenguaje bloquea al programador que intente escribir código malicioso: no se permite el acceso directo a memoria ni desbordar un arreglo, entre otras cosas. Y hay una segunda capa, el modelo sandbox: la JVM ejecuta el bytecode en un entorno controlado, así que aunque algo se escape al compilar, las consecuencias no llegan a la máquina real.

La máquina virtual

La JVM (Java Virtual Machine) es un software —un programa nativo, escrito y compilado para una plataforma concreta— que interpreta y ejecuta el bytecode generado por el compilador de Java.

Según la propia definición, es una máquina de computación de tipo abstracto: emula a una máquina real, tiene su propio conjunto de instrucciones y administra sus propias áreas de memoria. Lo interesante es que no asume ninguna tecnología en particular —por eso es portátil— y que tampoco está condenada a interpretar: puede generar código nativo de la plataforma, e incluso aceptar como entrada código objeto ya compilado, como funciones nativas de C. Se puede implementar en sistemas embebidos o directamente en el procesador.

El camino de un programa Java: el archivo fuente se compila una sola vez a bytecode, y ese bytecode lo ejecuta la máquina virtual de cada sistema, que lo traduce al código de la máquina con el compilador JIT. Compilar una vez, ejecutar en cualquier sistema HolaMundo.java el código que escribimos javac bytecode HolaMundo.class LA MÁQUINA VIRTUAL, EN CADA SISTEMA JVM en Windows con su compilador JIT JVM en GNU/Linux con su compilador JIT JVM en macOS con su compilador JIT JVM en un celular con su compilador JIT El mismo archivo .class sirve para los cuatro: cada JVM lo traduce al código de su propia máquina.
El .class se compila una sola vez y sirve para todos los sistemas. Lo único distinto en cada uno es la máquina virtual que lo ejecuta.

El compilador JIT

Si la JVM solo interpretara el bytecode instrucción por instrucción, los programas serían lentísimos. Por eso adentro tiene un compilador JIT (Just In Time, «justo a tiempo»), que carga el bytecode y lo traduce de a partes, de manera optimizada, con una precaución importante: no traduce dos veces lo mismo. Las partes del programa que más se usan se traducen a código nativo y quedan así, de modo que el programa se va acelerando a medida que se ejecuta.

Es decir: Java es compilado y interpretado, y esa combinación es la que le permite ser rápido sin dejar de ser portátil.

JDK, JRE y JAR

Para trabajar con Java hay dos paquetes que se confunden todo el tiempo, y la diferencia importa:

Qué trae Para qué sirve
JDK (Development Kit) El compilador, las herramientas de prueba y empaquetado, la biblioteca estándar y una JVM. Para programar. Es el que hay que instalar en la máquina donde se escribe el código.
JRE (Runtime Environment) Solo la biblioteca estándar y una JVM, sin el compilador ni las herramientas. Para ejecutar. Es más chico en disco y se puede redistribuir sin problemas de licencia: es el que se instala donde solo se van a correr los programas.

Los dos incluyen la JVM, que es lo que hace falta para que cualquier programa de Java arranque. Y el JDK agrega una herramienta que aparece cada vez que se entrega un trabajo: el empaquetador de JAR, que comprime un proyecto entero en un solo archivo. Un JAR es un archivo compatible con ZIP —se puede abrir con cualquier descompresor— que adentro lleva las clases compiladas y los recursos, y que se puede ejecutar directamente con doble clic si tiene declarada su clase principal.

Una última cosa, práctica: el JDK no es cómodo como entorno de trabajo, porque funciona únicamente con comandos de consola. Por eso se usa un IDE (Integrated Development Environment, entorno de desarrollo integrado) —NetBeans, Eclipse, IntelliJ— que junta el editor, el compilador, el depurador y el manejo de archivos en una sola ventana. Las instrucciones para instalar el JDK y NetBeans están en Java, en el anexo.

Las ediciones de Java

Java no es un solo producto: hay varias ediciones, cada una pensada para un ambiente distinto. Comparten el lenguaje y la máquina virtual, y se diferencian en la biblioteca que traen.

Edición Ambiente Qué agrega
Java SE
Standard Edition
Computadoras de escritorio y portátiles. La biblioteca básica: es la que hay que instalar para programar, aunque el programa esté destinado a otra edición. Se la conoce también como Java Desktop y hasta la versión 5 se llamó J2SE.
Java EE
Enterprise Edition
Servidores, aplicaciones empresariales. Todo lo de SE más las extensiones para servidores: acceso a bases de datos (JDBC), directorios distribuidos (JNDI), llamadas a métodos remotos (RMI/CORBA), correo (JavaMail) y aplicaciones web (Servlets y JSP). Se llama Jakarta EE desde que Oracle donó el proyecto a la Fundación Eclipse. Se desarrolla en la lección 9.
Java ME
Micro Edition
Dispositivos móviles y de consumo. Una versión recortada de SE con extensiones para las limitaciones de esos equipos: teléfonos, smartphones, PDAs, electrodomésticos. Fue la vía de los juegos para celular durante años, porque se podía probar en la computadora y después cargar al teléfono.
Java Card Tarjetas con chip. Una versión muy recortada para las tarjetas de crédito y similares. En esas tarjetas el sistema operativo es una pequeña JVM, y las aplicaciones se cargan después.

La POO como base de Java

Esto ya se vio en la lección 1; acá se trata de reconocer cómo lo nombra Java y qué agrega el lenguaje. La definición del paradigma es la misma: una forma de organización del conocimiento en la que las entidades centrales son los objetos. En un objeto se unen los datos —que Java llama atributos o variables de instancia— con las rutinas que los manipulan —los métodos—. Los objetos se comunican mediante mensajes, y cada mensaje está asociado a un método: cuando el objeto lo recibe, ejecuta el método asociado.

Y la aclaración que siempre hay que repetir: cuando se escribe un programa orientado a objetos no se definen objetos, se definen clases. La clase es la plantilla; los objetos se crean al declarar una variable de esa clase, y a eso se lo llama instanciar.

Los apuntes mencionan acá dos métodos especiales que en Java no son simétricos, y vale la pena saberlo desde el principio:

  • Los constructores se llaman siempre que se crea un objeto de esa clase, y su misión es iniciar el objeto. Java los tiene, se pueden sobrecargar y se ven en detalle en la lección 3.
  • Los destructores existen en otros lenguajes y su misión es hacer la tarea final antes de destruir el objeto. Java no los tiene, porque la memoria la libera el recolector de basura sin que uno se entere. Lo más parecido es finalize(), del que tampoco conviene depender.

Y las tres propiedades fundamentales del paradigma, con el nombre que les da Java: el encapsulamiento tiene como base la clase, que define la estructura y el comportamiento compartidos por el grupo de objetos; la herencia permite crear clases descendientes que reciben las propiedades de las existentes, y es el mecanismo con el que Java reutiliza código; el polimorfismo hace que un mismo mensaje pueda actuar sobre objetos de distinto tipo y comportarse de manera distinta en cada uno.

Tipos de datos

Java tiene ocho tipos primitivos y una clase que se usa como si lo fuera. Los tamaños son fijos, y esa es la característica que garantiza que el mismo código se comporte igual en cualquier máquina.

Tipo Tamaño Rango de valores Para qué
Números enteros (complemento a 2)
byte8 bits-128 a 127Entero de un byte
short16 bits-32.768 a 32.767Entero corto
int32 bits-2.147.483.648 a 2.147.483.647El entero de uso general
long64 bits-9.223.372.036.854.775.808 a 9.223.372.036.854.775.807Entero largo
Números reales (IEEE 754)
float32 bits±3,4E+38 (7 cifras decimales)Coma flotante de precisión simple
double64 bits±1,8E+308 (15 cifras decimales)Coma flotante de precisión doble
Otros
boolean8 bitstrue y falseUn valor lógico
char16 bitsUn carácter UnicodeUn solo carácter
StringVariableConjunto de caracteresCadena de caracteres es una clase, no un primitivo

Conversiones entre tipos

Cuando el valor de un tipo tiene que pasar a otro, se lo indica escribiendo el tipo nuevo entre paréntesis delante de la expresión. Eso se llama castear o convertir:

double centigrados = ((fahrenheit - 32.0) * 5.0) / 9.0;
int temperatura = (int) centigrados;

Ojo con la segunda línea: el (int) no redondea, corta la parte decimal. Si centigrados vale 36,8, la variable temperatura va a valer 36. Perder precisión sin querer es uno de los errores más comunes al empezar.

El caso especial de String

Las cadenas de caracteres no son un tipo primitivo: son objetos, y Java proporciona la clase String para crearlas y manipularlas. Se declaran así:

String nombre = "Lucía";

Esa es la forma básica, aunque la clase tiene más de diez constructores distintos. Y como es un objeto, tiene métodos propios para trabajar con ella:

nombre.length()       // 5, la cantidad de caracteres
nombre.concat(" G.")  // "Lucía G.", pega otra cadena
nombre.toUpperCase()  // "LUCÍA"

System.out.println(nombre.toUpperCase());   // imprime LUCÍA

Que String sea una clase y no un primitivo no es un detalle: es la razón por la que una cadena tiene métodos (length(), toUpperCase()) y un int no. Es también el primer lugar donde se ve que en Java casi todo es un objeto.

Operadores y palabras reservadas

Aritméticos

OperadorSignificadoOperadorSignificado
++Incremento*Multiplicación
--Decremento/División
+Unario positivo%Resto de la división entera (módulo)
-Unario negativo+Suma
-Resta

Ojo con ++ y --: puestos adelante o atrás del nombre cambian el momento en que se aplica el incremento. ++a incrementa y después usa el valor; a++ usa el valor y después incrementa. Dentro de una misma instrucción, la diferencia se nota.

Relacionales y lógicos

OperadorSignificadoOperadorSignificado
==Igual (comparación)>=Mayor o igual que
!=Distinto a<=Menor o igual que
>Mayor que!Negado (NOT)
<Menor que&& / ||Conjunción (AND) / disyunción (OR)

De asignación

OperadorSe escribeEquivale aOperadorSe escribeEquivale a
=a = 5a = 5*=a *= 2a = a * 2
+=a += 4a = a + 4/=a /= 3a = a / 3
-=a -= 3a = a - 3%=a %= 4a = a % 4
++ y --: a++, ++a, a--, --a equivalen a a = a + 1 y a = a - 1

Precedencia de operadores

Cuando hay varios operadores en la misma expresión, se aplican en este orden, de mayor a menor prioridad:

OperadoresCuáles son
Posfijos[ ], ( ), a++, a--
Pre y unarios++a, --a, +a, -a, ~, !
Creación o conversiónnew, (tipo) a
Multiplicación*, /, %
Suma+, -
Comparación==, !=, <, >, <=, >=
Lógicos&&, ||
Condicional? :  — como en (a > b) ? a : b
Asignación=, +=, -=, *=, /=, &=

Para no tener que memorizarla, la salida fácil son los paréntesis: (a + b) * c se entiende sin consultar ninguna tabla.

Palabras reservadas

Estas son las palabras que Java usa para su propio vocabulario. No se pueden usar como nombres de variables, clases ni métodos:

abstract, boolean, break, byte, case, catch, char, class, const*, continue default, do, double, else, extends, false, final, finally, float, for goto*, if, implements, import, instanceof, int, interface, long, native, new null, package, private, protected, public, return, short, static, strictfp, super switch, synchronized, this, throw, throws, transient, true, try, void, volatile, while

Las dos marcadas con asterisco —const y goto— están reservadas pero no se usan: se dejaron apartadas solo para que nadie las use como nombre. Ojo también con una trampa: el apunte original lista en su lugar dos palabras mal escritas («rest» y «strictftp»); las correctas son return y strictfp.

Variables, referencias y ámbito

Cómo se declaran

Una variable se define indicando el tipo y el nombre: int numero;. Pueden ser de un tipo primitivo o referencias a objetos de alguna clase, sea de la biblioteca o escrita por nosotros.

Lo que pasa si no se les da un valor inicial es distinto según el caso, y es una fuente clásica de sorpresas:

  • Los atributos de una clase se inicializan solos: los numéricos a cero, los boolean a false, los char al carácter nulo '\0' y las referencias a null.
  • Las variables locales —las de adentro de un método— no se inicializan solas: el compilador obliga a darles un valor antes de usarlas. Si se intenta leer una sin asignar, el programa no compila.

Constantes

Para declarar una constante simbólica se antepone la palabra reservada final:

final double PI = 3.1416;

Si la constante es un miembro de la clase —un atributo— conviene agregar static, para que exista una sola para todos los objetos de esa clase en lugar de una copia por objeto:

final static int MAX = 600;

El tipo void

La palabra void se usa para indicar que un método no devuelve nada. No es un tipo de dato: es la ausencia de tipo.

Referencias y objetos: no son lo mismo

Esta es la distinción más importante de toda la sección, y la que explica los errores más frecuentes. Una referencia es una variable que indica dónde está guardado un objeto en la memoria. A diferencia de C++, Java no permite acceder al valor de esa dirección: los punteros se eliminaron del lenguaje.

  • Al declarar una referencia, todavía no apunta a ningún objeto —salvo que se cree el objeto en la misma declaración—, y por eso se le asigna null.
  • Para que apunte a un objeto nuevo hay que crearlo con el operador new, que reserva en memoria el espacio para ese objeto, con sus atributos y sus métodos.
  • También se puede igualar la referencia a otra referencia existente, y entonces las dos apuntan al mismo objeto.

Esa última posibilidad es la que después produce el asombro de «cambié una y se modificaron las dos»: no son dos objetos, es uno solo con dos nombres.

Los vectores son referencias

Un tipo particular de referencias son los arrays o vectores —de variables primitivas o de objetos—. En su declaración hay que incluir los corchetes [ ], y se desarrollan más abajo, en su propia sección.

Visibilidad o ámbito

El ámbito de una variable es la parte del programa donde esa variable existe y se puede usar. En Java todas las variables están dentro de una clase, y en general una variable declarada dentro de un bloque —entre llaves { }— es visible y existe dentro de esas llaves:

  • Las variables declaradas al principio de un método existen mientras el método se ejecute.
  • Las declaradas dentro de un if dejan de ser válidas al terminar ese if.
  • Las variables miembro —los atributos, declarados entre las llaves de la clase y fuera de todo método— son válidas mientras exista el objeto.

Cuando un nombre tapa a otro: this

Java permite declarar una variable dentro de un bloque con el mismo nombre que un atributo. La variable del bloque oculta al atributo dentro de ese bloque. Para llegar al atributo oculto hay que usar this, que es la referencia al objeto actual:

public void setEdad(int edad) {
    this.edad = edad;   // this.edad es el atributo; edad es el parámetro
}

Por eso this aparece tanto en los constructores y en los set: es la forma de distinguir el atributo del parámetro cuando llevan el mismo nombre, que es lo más cómodo y lo más común.

Estructuras de control

Son las que deciden el orden en que se ejecutan las instrucciones: sin ellas un programa haría siempre lo mismo, de arriba hacia abajo, una sola vez.

Estructuras de condición: if

Ejecutan una instrucción o un bloque según se cumpla una condición. Tiene estas cuatro formas:

// Una sola instrucción, sin llaves
if (condicion)
    instruccion;

// Varias instrucciones: hacen falta las llaves
if (condicion) {
    varias instrucciones
}

// Dos caminos: el else
if (condicion) {
    varias instrucciones
} else {
    otras instrucciones
}

// Una instrucción de un lado y varias del otro
if (condicion)
    instruccion;
else {
    varias instrucciones
}

Los paréntesis después del if son obligatorios, y las llaves son optativas cuando hay una sola instrucción. Con dos o más, hay que abrir el bloque. La forma con else permite elegir entre dos caminos:

if (nota >= 6) {
    System.out.println("Aprobado");
} else {
    System.out.println("A recuperar");
}

Y encadenando else if se eligen entre varios caminos, evaluando en orden:

if (nota >= 9) {
    System.out.println("Sobresaliente");
} else if (nota >= 7) {
    System.out.println("Bien");
} else if (nota >= 6) {
    System.out.println("Aprobado");
} else {
    System.out.println("A recuperar");
}

El operador condicional ? : es la versión abreviada de un if-else que asigna un valor: (a > b) ? a : b devuelve a si se cumple y b si no. Se lo llama el operador ternario porque tiene tres partes.

Condición múltiple: switch

switch (expresion) {
    case valor1:
        instruccion;
        break;
    case valor2:
        instruccion1;
        instruccion2;
        break;
    default:
        instruccion;
}

El valor de la expresión y el de las constantes tiene que ser de tipo char, byte, short o int. Y desde Java SE 7 también puede ser un String.

El funcionamiento: el intérprete busca una constante que coincida con el valor de la expresión, y si la encuentra ejecuta las sentencias de ahí en adelante hasta toparse con un break. Ese es el detalle que hay que entender: sin break, el programa sigue ejecutando los casos siguientes. Si ninguna constante coincide, se ejecuta el bloque default, que es opcional.

Estructuras repetitivas: while, do-while y for

Sirven para repetir un proceso mientras se cumpla una condición:

// while: primero pregunta, después ejecuta
while (condicion)
    instruccion;

// do-while: primero ejecuta, después pregunta (la última línea lleva ; )
do {
    varias instrucciones
} while (condicion);

// for: cuando se sabe de antemano cuántas veces hay que repetir
for (inicializacion; condicion; actualizacion)
    instruccion;

La diferencia entre while y do-while se nota solo si el bloque tiene que ejecutarse al menos una vez: en el do-while la condición se evalúa al final, así que el cuerpo se ejecuta siempre una vez, aunque la condición sea falsa desde el principio.

Vectores

Para manejar colecciones de datos del mismo tipo dentro de una sola variable se usan los vectores, también llamados arrays, arreglos o matrices. En Java los vectores son objetos, así que tienen métodos y propiedades. Por ejemplo, todos conocen su propia longitud en el campo length.

Cómo se declaran

Hay dos formas equivalentes; se puede poner los corchetes junto al tipo o junto al nombre:

tipo nombreDelVector[ ];
tipo[ ] nombreDelVector;

Un vector se puede inicializar en el momento en que se declara, y en ese caso no hace falta indicar la cantidad de elementos: se reserva el espacio justo para los que se cargan.

String diasLaborables[] = { "Lunes", "Martes", "Miércoles", "Jueves", "Viernes" };

Si no se inicializa, la declaración sola no reserva memoria. Hay que llamar explícitamente al constructor con new y decir cuántos elementos va a tener:

double vector[] = new double[5];

int matriz[][];
matriz = new int[4][7];   // 4 filas y 7 columnas
Un vector de cinco elementos: cada casilla tiene su índice, y la numeración empieza en cero, así que la última posición es la cuatro. new double[5] — y por qué el último índice es 4 0.0 0.0 0.0 0.0 0.0 vector[0] vector[1] vector[2] vector[3] vector[4] el primero el último: length - 1 Y como todo lo demás arranca en cero, el vector queda con los cinco valores en 0.0.
El índice del primer elemento es 0 y el del último es la cantidad de elementos menos uno.

Cómo se usan

Para llegar a un elemento se usa el nombre del vector y el índice entre corchetes: vector[2] = 4.5;. El índice del primero es 0 y el del último, la cantidad de elementos menos uno.

Y hay una diferencia importante con C: intentar acceder a una posición fuera del rango provoca una excepción. Si el programa no la maneja, el error corta la ejecución con un mensaje que dice exactamente qué pasó y en qué línea. Esto se ve en detalle en la lección 4, pero conviene saber desde ahora que el lenguaje avisa en vez de dejar que el programa lea memoria ajena.

El for mejorado

Para recorrer un vector completo sin usar un contador —y sin poder equivocarse con los índices— existe el for mejorado:

for (tipo parametro : nombreVector) {
    instrucciones
}

El parámetro tiene un tipo y un nombre —por ejemplo int numero— y su tipo tiene que coincidir con el de los elementos del vector. La instrucción recorre los valores uno por uno; su encabezado se lee así: «para cada iteración, asigná el siguiente elemento del vector a la variable parámetro, y después ejecutá la instrucción».

Se puede usar en lugar del for con contador siempre que no se necesite saber la posición del elemento actual. Si hay que imprimir el número de casilla o comparar con el anterior, hay que volver al for clásico.

Vectores como parámetros

Para pasar un vector a un método se escribe su nombre sin corchetes:

double[] temperaturasPorHora = new double[24];
modificarVector(temperaturasPorHora);

Y el método que lo recibe declara su parámetro con corchetes:

void modificarVector(double[] b) {
    // b y temperaturasPorHora son el mismo objeto vector
}

Lo que se copia al llamar al método es la referencia, no el vector. Así que b y temperaturasPorHora son dos nombres del mismo objeto: lo que el método modifique a través de b se ve en el vector original.

De ahí sale otra comodidad: como todo vector conoce su longitud, no hace falta pasar la cantidad de elementos como segundo parámetro. El método la consulta con b.length. Es una de esas cosas que en C no se pueden hacer y que en Java vienen incluidas.

La estructura de un programa

Como Java es un lenguaje orientado a objetos, un programa se compone solamente de objetos. Y como un objeto es la instanciación de una clase, todo programa empieza por escribir clases: unas cuantas van a venir de la biblioteca de Java, ya escritas y compiladas, y otras habrá que escribirlas según el problema.

Un programa se puede escribir con una sola clase, y a medida que la aplicación se complica el número de clases crece. Lo que no cambia nunca es que tiene que existir un punto de entrada: el método main, que es donde arranca la ejecución y desde donde se llaman todos los demás métodos.

La estructura mínima de un programa es esta:

public class Inicio {

    public static void main(String[] args) {
        System.out.println("Primera sentencia");
        System.out.println("Y la siguiente");
        // ...
        System.out.println("La última");
    } // cierre del método main

} // cierre de la clase

Tres cosas para mirar de ese ejemplo, porque son las que desconciertan el primer día:

  • Todo está adentro de una clase. No hay instrucciones sueltas en un archivo.
  • El nombre del archivo tiene que ser Inicio.java, igual que la clase pública, respetando mayúsculas y minúsculas.
  • El main es el punto de entrada y también el de salida: cuando termina su última instrucción, el programa termina.

Por qué el main se escribe siempre igual

public static void main(String[] args) {

Cada palabra de esa línea tiene un motivo, y ninguno es decorativo:

  • public — para que se pueda acceder desde afuera de la clase. La máquina virtual lo llama desde afuera, así que si fuera privado no lo encontraría.
  • static — porque es un método común a todos los objetos de la clase, y sobre todo porque tiene que poder llamarse sin haber creado ningún objeto. Cuando el programa arranca, todavía no existe nada; si main no fuera estático habría que instanciar un objeto antes de poder ejecutarlo, y no habría quién lo hiciera.
  • void — porque no devuelve nada. Un main que devuelve un valor no tendría quién lo reciba: el sistema operativo no espera un resultado.
  • String[] args — un vector de cadenas que guarda los argumentos que se pasan por la línea de comandos. Si el programa se ejecuta con java Inicio hola 15, adentro del main van a estar disponibles como args[0] y args[1].

Toda aplicación Java tiene un solo método main como punto de entrada.

Las partes de una clase

Siguiendo los lineamientos de la documentación de Java, una clase puede tener estas partes, en este orden:

  1. Package. Es la jerarquía de directorios donde está alojada la clase. Un paquete es un conjunto de clases relacionadas agrupadas bajo un nombre: sirve para organizarlas y encontrarlas más fácil. Para referirse a una clase de un paquete se usa su nombre completo —java.lang.System es la clase System del paquete java.lang— salvo que el paquete se haya importado. Las clases que uno escribe sin declarar paquete pertenecen al paquete predeterminado.
  2. Import. Las sentencias que invocan las clases y paquetes de otros paquetes que la clase necesita. Algunos paquetes se importan solos, sin declararlo, como java.lang. Se puede importar un paquete completo usando el asterisco como comodín: import java.util.*;
  3. Class. La definición del nombre de la clase.
  4. Variables de ámbito de clase. Los atributos que van a tener los objetos instanciados de esa clase, con esta forma:
    [modificador] tipoDato nombreAtributo [= valorInicial];
  5. Constructor. Un tipo especial de método que se invoca automáticamente al crear el objeto, y sirve para inicializarlo y dejar lista cualquier configuración que la clase necesite. Se puede sobrecargar.
  6. Métodos. Los bloques de código con las instrucciones que hacen las tareas del programa. Son, literalmente, la interfaz de la clase: lo que la clase muestra al mundo. Su definición tiene una cabecera y un cuerpo entre llaves:
    [modificador] tipoResultado nombreMetodo(lista de parametros) {
        declaraciones de variables locales;
        sentencias;
        [return (expresion)]
    }
    Si el tipo de resultado es void, el método no devuelve nada y no hace falta la sentencia return.

Los modificadores de acceso

Los modificadores indican el nivel de protección de un atributo o un método. Son cuatro, y esta tabla es de las que conviene tener a mano:

Modificador Quién puede acceder
privateÚnicamente los métodos de su propia clase. De afuera no se ve, ni desde una clase derivada.
publicCualquier método, esté dentro o fuera de la clase y del paquete.
protectedLa propia clase y las clases que heredan de ella.
sin modificadorNivel de paquete, que es el predeterminado: accesible desde todas las clases del mismo paquete.

Generalmente los atributos se declaran privados y se accede a ellos a través de los métodos públicos de la clase. Esa práctica es el encapsulamiento: ocultar la estructura interna de datos de un objeto y permitir el acceso solo por la interfaz pública.

Una última regla que explica muchos usos raros al principio: una clase derivada solo puede acceder a los miembros de su clase base declarados public o protected, nunca a los private. Y el modificador static en un atributo hace que la información sea común a todos los objetos de la clase, y que exista aunque no se haya creado ninguno.

Crear y usar objetos

Las clases son plantillas para crear objetos, y para eso está el operador new:

CGrados grados = new CGrados();

Después de new va el nombre de la clase y paréntesis: eso no es decoración, son los paréntesis del constructor, el método que se llama en ese momento para armar el objeto.

La misma clase puede ofrecer varias formas de construir el objeto. Por ejemplo, la clase GregorianCalendar del paquete java.util, que representa una fecha y una hora:

GregorianCalendar fecha1 = new GregorianCalendar();
GregorianCalendar fecha2 = new GregorianCalendar(2016, 3, 22);
GregorianCalendar fecha3 = new GregorianCalendar(2016, 3, 22, 12, 45, 15);

Los tres objetos son válidos porque la clase proporciona varias formas de construirlo, y todas se diferencian en la cantidad de parámetros. Eso es la sobrecarga, que viene en la sección siguiente.

El constructor que regala Java

Java crea automáticamente un constructor sin parámetros para cualquier clase que uno defina. Pero hay una trampa: en cuanto se define un constructor propio, ese constructor por defecto deja de existir y hay que escribirlo si se lo quiere seguir usando. Es lo que explica que una clase que antes se instanciaba con new MiClase() deje de compilar después de agregarle un constructor con parámetros.

Qué pasa con la memoria

Cuando se crea un objeto con new, Java asigna automáticamente la cantidad de memoria necesaria para ubicarlo. Si no hubiera espacio disponible, el operador lanza una excepción OutOfMemoryError.

Y la memoria se libera sola: Java la recupera apenas el objeto deja de usarse, o sea cuando ya no queda ninguna referencia apuntándolo. De eso se ocupa el Garbage Collector (recolector de basura), un programa que busca objetos inalcanzables, los destruye y libera la memoria que ocupaban. Para llevar la cuenta, cada objeto registra por cuántas referencias es apuntado: si una referencia deja de existir pero hay otras, el objeto no se elimina.

Esto es lo que diferencia a Java de C++: no hay que acordarse de liberar nada. Y también es una de las cosas que se paga con el rendimiento del recolector, que trabaja mientras el programa corre.

Acceder a los miembros y mandar mensajes

Para llegar a un miembro de un objeto se escribe el identificador del objeto, un punto y el nombre del miembro:

cuenta1.saldo = 355;             // acceso a un atributo
cuenta1.asignarLimite(3000);     // envío de un mensaje

La segunda línea es la forma normal de trabajar: enviar un mensaje es invocar uno de sus métodos, y eso hace que el objeto realice la tarea que define ese método. La primera línea —tocar el atributo directamente— solo funciona si el atributo es público, y en un diseño con encapsulamiento no debería serlo. Es uno de esos ejemplos donde el material muestra la mecánica del lenguaje antes que la buena práctica: en el código propio, ese saldo se cambia con un método.

Sobrecarga de métodos

Java permite que existan varios métodos con el mismo nombre, siempre que su signatura no sea idéntica. La signatura es el nombre del método más los tipos de sus parámetros y el orden en que están:

public void saludar(String nombre) { }
public void saludar(String nombre, int edad) { }   // otra signatura: válido

public int promedio(int a, int b) { return (a + b) / 2; }
public int promedio(float a, float b) { return (int) (a + b) / 2; }   // válido

En cambio, no es válido definir dos métodos con el mismo nombre que se diferencien únicamente por el tipo del valor que devuelven:

public int calcular(int a) { return a; }
public double calcular(int a) { return a; }   // ERROR: misma signatura

El compilador no puede saber a cuál de los dos llamar, porque en la llamada calcular(5) no hay nada que indique el tipo que se espera. La sobrecarga es especialmente importante en los constructores, porque es lo que permite tener varias formas de crear un objeto, como en el ejemplo de GregorianCalendar.

El ejemplo más usado de sobrecarga sin darse cuenta es System.out.println, que tiene una versión para cada tipo:

System.out.println()          System.out.println(boolean)   System.out.println(char)
System.out.println(char[])    System.out.println(double)    System.out.println(float)
System.out.println(int)       System.out.println(long)      System.out.println(Object)
System.out.println(String)

Uno escribe siempre println y el compilador elige la versión según lo que va adentro de los paréntesis.

Cómo se pasan los parámetros

Cuando se llama a un método, ese método trabaja con una copia local de los parámetros: los valores con los que fue invocado. Ahora bien, como las variables de tipos no primitivos son en realidad referencias a objetos, lo que se copia en ese caso es la referencia, no el objeto.

La consecuencia práctica es esta: un método sí puede modificar el estado de un objeto que recibe como parámetro, invocando métodos de ese objeto. Lo que no puede hacer es cambiar cuál es el objeto al que apunta la variable del que llamó: la referencia se copió. Es exactamente lo que se vio con los vectores, y vale para cualquier objeto.

Recursividad

Un método puede llamarse a sí mismo. Eso es la recursividad, y sirve para los problemas que se describen en términos de sí mismos: el factorial de un número es ese número multiplicado por el factorial del anterior; lo que pesa una carpeta es lo que pesan sus archivos más lo que pesa cada subcarpeta. En los dos casos la definición vuelve a nombrar la cosa que está definiendo, y el código puede hacer lo mismo.

Para que la recursión termine —y esto es lo único que hay que entender bien— hacen falta dos condiciones:

  • Un caso base. Una situación que se resuelve directamente, sin volver a llamar a nadie. Es lo que corta la cadena.
  • Que cada llamada reduzca el problema. Si la segunda llamada es igual de grande que la primera, el caso base nunca se va a alcanzar.

El factorial cumple las dos. Su definición matemática ya es recursiva:

F(0) = 1     y     F(n) = n × F(n − 1)

La primera parte es el caso base y la segunda, la reducción. Traducida a Java queda casi literal:

El factorial, paso a paso

public class Factorial {

    public static void main(String[] args) {
        System.out.println("factorial(5) = " + factorial(5));
    }

    static long factorial(int n) {
        if (n == 0) {
            return 1;                    // caso base: acá se corta la cadena
        }
        return n * factorial(n - 1);     // reducción: el problema se achica
    }
}
factorial(5) = 120

Lo que hace la máquina cuando ejecuta eso es lo importante. Al llamar a factorial(5), el método no puede devolver nada todavía: necesita el resultado de factorial(4) para multiplicarlo por 5. Entonces deja su cálculo pendiente y llama a otro, que hace lo mismo, hasta llegar a factorial(0). Ese es el único que devuelve un valor sin pedir nada. Recién ahí empieza la vuelta: factorial(1) multiplica y devuelve 1, factorial(2) devuelve 2, y así hasta el 120.

Las llamadas de factorial(4) se apilan hacia abajo hasta el caso base factorial(0), que devuelve 1; después cada llamada vuelve multiplicando: 1, 1, 2, 6 y 24. factorial(4): primero la ida, después la vuelta IDA · las llamadas se apilan VUELTA · los valores regresan factorial(4) espera 4 × factorial(3) espera 3 × factorial(2) espera 2 × factorial(1) espera 1 × factorial(0) ← caso base 4 × 6 = 24 3 × 2 = 6 2 × 1 = 2 1 × 1 = 1 devuelve 1 El caso base es el único que devuelve un valor sin llamar a nadie: por eso la cadena se detiene ahí. Los demás no pueden terminar hasta que vuelva la llamada de abajo, porque necesitan ese valor para multiplicarlo.
Cada llamada pendiente ocupa un lugar en la pila mientras espera. La ida baja hasta el caso base; la vuelta sube multiplicando.

Hay una manera muy simple de ver esas dos etapas en el resultado de un programa. Este método imprime antes de llamarse a sí mismo, así que la salida sale en el mismo orden que la ida:

static void contarHastaUno(int x) {
    if (x > 0) {
        System.out.println(x);
        contarHastaUno(x - 1);
    }
}

Llamado con 5 imprime 5 4 3 2 1. Y si se mueve una sola línea —el println pasa después de la llamada— la salida se da vuelta:

static void contarDesdeUno(int x) {
    if (x > 0) {
        contarDesdeUno(x - 1);
        System.out.println(x);
    }
}

Ahora imprime 1 2 3 4 5. El recorrido de las llamadas es idéntico en los dos casos; lo único que cambió es en qué momento se ejecuta la impresión. Es un buen truco para entender la recursión: el código que está antes de la llamada pertenece a la ida, y el que está después pertenece a la vuelta.

Qué pasa si falta el caso base

Es el error más común, y tiene un síntoma muy claro. Si se quita el if (n == 0) y el método se llama a sí mismo sin ninguna condición, el programa no entra en un bucle infinito: como cada llamada pendiente ocupa un lugar en la memoria, llega un momento en que no hay más lugar. Ahí la máquina virtual corta la ejecución con un error:

Exception in thread "main" java.lang.StackOverflowError

Tres detalles de ese error que conviene saber:

  • Es un Error, no una excepción. Su jerarquía es StackOverflowError → VirtualMachineError → Error → Throwable. Es de la rama que el programa no está obligado a manejar, porque no es un problema de la lógica del programa sino de los recursos de la máquina. La jerarquía completa está en la lección 4.
  • El error no trae mensaje: el nombre de la clase ya dice todo. Y si se lo captura, el programa sigue, pero la recursión no se puede «continuar»: se perdió la llamada que faltaba.
  • La cantidad de llamadas que entran depende del tamaño de la pila, no del método. Con una recursión de este tipo, en una máquina con la pila por defecto entran del orden de veinte mil llamadas; el doble de pila admite bastante más de cuarenta mil. Ese tamaño se cambia con el parámetro -Xss de la máquina virtual.

Por la misma razón, la recursión no sirve para contar hasta un millón. El lenguaje tampoco la optimiza: Java no transforma la recursión de cola en un bucle, así que un método que se llama a sí mismo como última instrucción sigue consumiendo un lugar en la pila por llamada. Si hay que repetir algo un millón de veces, va un for.

¿Recursión o bucle?

Todo lo que se escribe con recursión se puede escribir con un bucle, y al revés. Entonces la pregunta no es cuál se puede, sino cuál conviene:

  • El bucle no gasta memoria extra: repite con las mismas variables. Para recorrer un vector, contar o acumular, es la opción natural y más rápida.
  • La recursión brilla cuando el problema ya es recursivo: una estructura con partes que contienen lo mismo —una carpeta con subcarpetas, un árbol, un documento con secciones adentro de secciones—. Ahí el código recursivo suele ser mucho más corto y más fácil de leer que el bucle con una pila manejada a mano.

Hay además una trampa que aparece siempre y que no tiene nada que ver con la recursión: el tipo de dato se queda corto antes que la recursión. El factorial crece muy rápido, y los tipos tienen un techo:

LlamadaResultado que se esperaLo que imprime el programaPor qué
factorial(12)479 001 600479001600Entra en un int, que llega hasta 2 147 483 647.
factorial(13)6 227 020 8001932053504Ya no entra en un int: el número da la vuelta y el resultado es falso.
factorial(20)2 432 902 008 176 640 0002432902008176640000Es el último que entra en un long.
factorial(21)51 090 942 171 709 440 000-4249290049419214848Tampoco entra en un long. Un factorial negativo es imposible: es un desborde.

Convenciones de programación

No son reglas del lenguaje —el programa compila igual si no se respetan— pero sí son el acuerdo que usa todo el mundo. Y tienen una razón de ser: cualquiera que lea el código, incluido uno mismo seis meses después, tiene que poder darse cuenta de qué es cada cosa por cómo está escrita.

QuéCómo se nombraEjemplo
Paquetes Sustantivos, todo en minúsculas. package conversor.temperatura
Clases Sustantivos, con la primera letra de cada palabra en mayúscula. El archivo .java tiene que llamarse igual que la clase, respetando mayúsculas: en Java MiClase y miclase son distintos. class MiClase
Interfaces Igual que las clases. interface CuentaCorriente
Métodos Verbos, la primera letra en minúscula y las siguientes palabras capitalizadas. asignarTemperatura()
Variables Combinaciones de palabras, primera en minúscula y las siguientes capitalizadas. Sin guiones bajos y sin el signo $, que tiene un significado especial. El nombre tiene que decir qué contiene: evitar i, j, k salvo como contadores de un ciclo. char primerLetra
Constantes Todo en mayúsculas, palabras separadas con guion bajo. static final double PI = 3.1416;

Formato del código

  • Llaves { } siempre, incluso cuando la estructura de control tiene una sola instrucción. Ponerlas después evita el error clásico de agregar una línea y que quede fuera del if.
  • Una sentencia por línea, y líneas vacías para separar ideas. Es una de las cosas que hace legible el código.
  • Sangrado. Lo que está entre llaves se indenta, para que se vea de un golpe dónde empieza y dónde termina cada bloque.

Comentarios

Se usan para explicar los segmentos de código que no son obvios. Java tiene tres formas:

// Comenta una sola línea

/* Comenta una extensión mayor
   de código o de texto */

/** Comentario de documentación, para Javadoc
 *  @author nombre del autor del código
 *  @see otra clase para más información
 */

La tercera es especial: con el formato /** */ y sus etiquetas, la herramienta Javadoc recorre el código y genera un sitio HTML con la documentación, tomando esas notas. Es la razón por la que las bibliotecas de Java tienen documentación: se escribe al mismo tiempo que el código. La etiqueta @see agrega un «ver también» con un enlace a otra clase o método.

Ejercicios

Nueve ejercicios sobre lo que se vio en esta lección.

1. Los tres paquetes

Explicar con una frase para qué sirve cada uno y qué pasa si en una máquina que solo va a ejecutar un programa se instala el JRE en lugar del JDK.

2. Tipos de datos

Decidir qué tipo conviene para cada dato y justificar: la edad de una persona, el precio de un producto con centavos, la cantidad de habitantes del país, el nombre de una calle, y si un alumno está presente.

3. Qué imprime

Escribir el resultado de cada instrucción, sabiendo que a vale 7:

System.out.println(a / 2);
System.out.println(a % 2);
System.out.println(a > 5 && a < 10);
System.out.println((a > 5) ? "alto" : "bajo");
System.out.println(a++ + " y después " + a);

4. Vectores

Escribir un programa que declare un vector de cinco notas, las cargue con valores, y calcule e imprima el promedio y la nota más alta. Hacerlo dos veces: una con for con contador y otra con el for mejorado. Después responder: ¿por qué el segundo no permite calcular la posición de la nota más alta?

5. El main

Explicar por qué main tiene que ser static y qué pasa si se lo declara sin ese modificador. Después indicar cómo se leen los argumentos si el programa se ejecuta como java Promedio 8 7 9.

6. Sobrecarga

Escribir una clase Calculadora con tres métodos sobrecargados que se llamen multiplicar y sirvan para dos enteros, para tres enteros y para dos números decimales. Después responder por qué no se puede definir un cuarto multiplicar que se diferencie de uno de los anteriores solo por el tipo que devuelve.

7. Recursividad

Escribir un método recursivo sumaDigitos(int n) que devuelva la suma de los dígitos de un número: sumaDigitos(1234) tiene que dar 10. Después responder: ¿cuál es el caso base de este método y cuál es la reducción? Y si se llama a factorial(21) con el método de la lección, ¿por qué el resultado que imprime el programa es un número negativo?

8. Matrices

Cargar dos matrices de 3 × 3 y calcular: la suma de las dos, cada una multiplicada por un número, y el producto de las dos. Después explicar por qué el producto necesita tres for y no alcanzan dos.

9. Buscar en un vector

Escribir un método buscar(int[] vector, int valor) que devuelva la posición donde está el valor y -1 si no está. Después responder por qué para «no está» no se puede devolver 0.

Respuestas sugeridas

1. El JDK sirve para programar: trae el compilador. El JRE sirve para ejecutar: trae solo lo necesario para correr programas ya compilados, y por eso es más chico y se puede redistribuir. En una máquina de ejecución el JRE alcanza y sobra; el compilador no se usa nunca y ocupa espacio. El JAR es el archivo comprimido en el que se entrega un proyecto completo para distribuirlo.

2. La edad: byte alcanza —va de -128 a 127—, aunque int es lo habitual y no complica nada. El precio con centavos: double, o mejor float si no hace falta tanta precisión. La cantidad de habitantes: long, porque puede pasar los dos mil millones de int en países grandes. El nombre de una calle: String. Si está presente: boolean. Es la primera vez que aparece la idea de elegir el tipo según el dato, y el criterio es siempre el mismo: qué valores puede tomar y si entra en el rango.

3.

  • a / 2 → 3, no 3,5: los dos son enteros, así que la división es entera y descarta el resto.
  • a % 2 → 1: el resto de dividir 7 por 2.
  • a > 5 && a < 10 → true, se cumplen las dos condiciones.
  • (a > 5) ? "alto" : "bajo" → alto.
  • a++ + " y después " + a → 7 y después 8. Acá está el detalle de los operadores: a++ devuelve el valor antes de incrementar, así que en la primera parte aparece 7; cuando se evalúa a al final, ya vale 8.

4. Con contador:

double[] notas = { 8.5, 4, 10, 7, 6 };
double suma = 0;
double mayor = notas[0];

for (int i = 0; i < notas.length; i++) {
    suma = suma + notas[i];
    if (notas[i] > mayor) {
        mayor = notas[i];
    }
}
System.out.println("Promedio: " + suma / notas.length);
System.out.println("Mayor: " + mayor);

Con el for mejorado el promedio sigue saliendo, pero la mayor no se puede ubicar:

double suma = 0;
double mayor = notas[0];

for (double nota : notas) {
    suma = suma + nota;
    if (nota > mayor) {
        mayor = nota;
    }
}

El for mejorado entrega el valor de cada casilla, no su posición. Si lo que hace falta es el número de casilla —para decir «la mejor nota fue la cuarta»— hay que usar el contador. Ese es el criterio para elegir uno u otro.

5. main tiene que ser static porque la máquina virtual lo llama antes de que exista cualquier objeto del programa: si no fuera estático, habría que instanciar una clase para poder ejecutarlo, y no habría nadie que hiciera esa instancia. Sin static, el programa compila pero la máquina virtual avisa que no encuentra un punto de entrada. Con java Promedio 8 7 9, adentro del método los valores quedan en args[0] = "8", args[1] = "7" y args[2] = "9", siempre como texto: para sumarlos hay que convertirlos, por ejemplo con Integer.parseInt(args[0]).

6.

public class Calculadora {

    public int multiplicar(int a, int b) {
        return a * b;
    }

    public int multiplicar(int a, int b, int c) {
        return a * b * c;
    }

    public double multiplicar(double a, double b) {
        return a * b;
    }
}

Los tres conviven porque sus signaturas son distintas: cambia la cantidad de parámetros o el tipo de ellos. No se puede agregar un double multiplicar(int a, int b) porque su signatura —nombre más tipos de los parámetros— es idéntica a la del primero: el tipo que devuelve no forma parte de la signatura, así que el compilador no tendría cómo decidir a cuál llamar.

7.

static int sumaDigitos(int n) {
    if (n < 10) {
        return n;                          // caso base: ya es un solo dígito
    }
    return n % 10 + sumaDigitos(n / 10);    // reducción: se saca el último dígito
}

El caso base es que el número tenga un solo dígito: ahí no hay nada que sumar y se devuelve el número. La reducción es n / 10, que es una división entera: va tirando el último dígito, así que el número se achica en cada llamada y tarde o temprano llega a un dígito. El dígito que se descarta se recupera con n % 10, que es el resto de la división, y eso es lo que se suma al volver.

factorial(21) da negativo por el tipo, no por la recursión. El resultado real es 51 090 942 171 709 440 000 y no entra en un long, que llega hasta 9 223 372 036 854 775 807: al pasarse, el número «da la vuelta» y sigue contando desde el otro extremo del rango. La prueba de que la recursión no tiene la culpa es que la versión con un for da exactamente el mismo resultado. Para trabajar con números así de grandes hay una clase preparada, java.math.BigInteger, que no tiene techo:

import java.math.BigInteger;

BigInteger factorial = BigInteger.ONE;

for (int i = 2; i <= 21; i++) {
    factorial = factorial.multiply(BigInteger.valueOf(i));
}

System.out.println(factorial);   // 51090942171709440000

8. La suma recorre las dos matrices con dos for y guarda el resultado en una tercera:

int[][] suma = new int[3][3];

for (int f = 0; f < 3; f++) {
    for (int c = 0; c < 3; c++) {
        suma[f][c] = a[f][c] + b[f][c];
    }
}

El producto necesita un tercer contador porque cada casilla del resultado es una suma. La casilla de la fila f y la columna c se arma multiplicando toda la fila f de la primera matriz por toda la columna c de la segunda, y sumando esos tres productos:

int[][] producto = new int[3][3];

for (int f = 0; f < 3; f++) {
    for (int c = 0; c < 3; c++) {
        for (int k = 0; k < 3; k++) {
            producto[f][c] = producto[f][c] + a[f][k] * b[k][c];
        }
    }
}

Los dos primeros for eligen la casilla del resultado; el tercero, con la variable k, hace la sumatoria. Con una matriz { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } y otra { 1, 0, 2 }, { 0, 1, 0 }, { 3, 0, 1 }, la suma da { 2, 2, 5 }, { 4, 6, 6 }, { 10, 8, 10 } y el producto, { 10, 2, 5 }, { 22, 5, 14 }, { 34, 8, 23 }. Se puede comprobar a mano la primera casilla del producto: 1 × 1 + 2 × 0 + 3 × 3 = 10.

9.

static int buscar(int[] vector, int valor) {
    for (int i = 0; i < vector.length; i++) {
        if (vector[i] == valor) {
            return i;
        }
    }
    return -1;
}

No se puede devolver 0 para «no está» porque el cero es una posición válida: es la del primer elemento. Con ese criterio no habría forma de distinguir «lo encontré en la primera casilla» de «no lo encontré». El -1 sirve justamente porque no puede ser un índice, y por eso es la convención que se usa en todas las bibliotecas de Java. Con el vector { 7, 12, 4, 9, 4, 33 }, la búsqueda de 4 devuelve 2 —la primera vez que aparece, porque el método corta en cuanto lo encuentra— y la de un valor que no está, -1.

Resumen

  • Java es un lenguaje, una plataforma, un entorno de ejecución y una biblioteca. Nació en 1991 como proyecto Green para electrodomésticos, se llamó Oak y se anunció en mayo de 1995. Desde 2009 pertenece a Oracle.
  • Su rasgo distintivo es el bytecode: un código intermedio que no depende de la máquina, y que ejecuta la JVM con un compilador JIT que traduce al vuelo y no repite trabajo. De ahí sale el «escribí una vez, ejecutá donde sea».
  • El JDK es para programar; el JRE, para ejecutar; el JAR es el paquete en el que se entrega un proyecto. Las ediciones (SE, EE, ME, Card) son el mismo lenguaje con bibliotecas distintas según el ambiente.
  • Los tipos primitivos tienen tamaño fijo en cualquier plataforma, y String no es un primitivo sino una clase. La conversión se hace con el tipo entre paréntesis, y corta los decimales en vez de redondear.
  • Una variable es un nombre con un tipo; una referencia es una variable que apunta a un objeto, y no es lo mismo que el objeto. Los atributos se inicializan solos; las variables locales, no.
  • El ámbito es la parte del programa donde la variable existe. this sirve para llegar al atributo cuando un parámetro lo tapa.
  • Las estructuras de control son if, switch, while, do-while y for, más el operador ternario ? :. Los vectores son objetos, se indexan desde el cero, conocen su longitud y se pasan a los métodos por referencia.
  • Todo programa es una clase con un método main public static void, y una clase tiene package, import, class, atributos, constructor y métodos. Los modificadores deciden quién ve qué.
  • La sobrecarga permite varios métodos con el mismo nombre si cambia la signatura; los parámetros se pasan por valor, copiando la referencia cuando son objetos.
  • Las convenciones de nombres y de formato no son capricho: el código lo va a leer alguien más.

Con esto ya está el lenguaje en la mano. Lo que falta es lo que hace que Java sea Java: escribir clases propias, heredar, implementar interfaces y entender cómo se conectan los objetos. Eso es la lección 3.

Licencia. Obra derivada de los apuntes de clase del Prof. Matías E. García —Clase Teórica 1—, publicada en profmatiasgarcia.com.ar bajo CC BY-NC-SA 2.5 Argentina; el desarrollo del texto, los diagramas y los ejercicios son de Prof Mauro Bobyk.