Strings, I/O, Formateo, and Parseo

A continuación dejo el resumen del sexto capítulo (Strings, I/O, Formatting, and Parsing) del libro "Sun Certified Programmer for Java 6 Study Guide".

String, StringBuilder y StringBuffer (Objetivo 3.1):
Un String es un objeto inmutable, una vez que el mismo se creó, no puede ser modificado. Cuando se realiza una concatenación de dos String, lo que hace la JVM es crear un nuevo objeto String con el valor de las dos cadenas concatenadas. Cuando se invoca un método del objeto String (por ej.: toUpperCase()), el método devuelve un nuevo objeto String con su correspondiente valor, no se modifica el valor original del String (con el que se invoco el método).
Algunos métodos de los objetos String:
  • public char charAt(int index): Devuelve el carácter alojado en el índice pasado como parámetro.
  • public String concat(String s): Agrega un String al final de otro (como el operador ‘+’).
  • public boolean equalsIgnoreCase(String s): Determina la igualdad de dos String, ignorando las diferencias de mayúsculas y minúsculas.
  • public int length(): Devuelve la cantidad de caracteres que componen un String.
  • public String replace(char old, char new): Reemplaza las ocurrencias de un carácter con otro nuevo.
  • public String substring(int begin) - public String substring(int begin, int end): Devuelve una parte de un String.
  • public String toLowerCase(): Devuelve el String con todos los caracteres convertidos a minúsculas.
  • public String toUpperCase(): Devuelve el String con todos los caracteres convertidos a mayúsculas.
  • public String trim(): Remueve los espacios en blanco del principio y final del String.
La clase StringBuilder (agregada en Java 5), posee exactamente los mismos métodos que la clase StringBuffer. La única diferencia entre ellas, es que la clase StringBuilder es más rápida debido a que sus métodos no están sincronizados. Estas dos clases se utilizan para realizar muchas modificaciones sobre los caracteres de una cadena. Los objetos de estas clases no crean un nuevo objeto cada vez que se modifican (como los objetos String), es por esto que son más eficientes para realizar muchas modificaciones sobre cadenas.
Algunos métodos de los objetos StringBuilder y StringBuffer:
  • public synchronized StringBuffer append(String s)
  • public StringBuilder delete(int start, int end)
  • public StringBuilder insert(int offset, String s)
  • public synchronized StringBuffer reverse()
  • public String toString()
Resumen (TWO-MINUTE DRILL):
  • Los objetos String son inmutables, pero las referencias a String no lo son.
  • Si se crea un nuevo objeto String, pero no se asigna a una variable de referencia, el mismo se pierde para el programa.
  • Si se redirecciona una variable referencia a un nuevo objeto String, el objeto String anterior se pierde.
  • Los métodos de objetos String utilizan los índices basados en cero, excepto el método substring(), el cuál utiliza su segundo argumento basado en uno.
  • La clase String es final, sus métodos no pueden ser sobreescritos.
  • Cuando la JVM encuentra un literal String, este se agrega al “String literal pool”.
  • Los objetos String poseen el método length(), los arreglos poseen la propiedad length.
  • La API de la clase StringBuffer es la misma que la de StringBuilder, salvo que los métodos de esta última no se encuentran sincronizados.
  • Los métodos de los objetos StringBuilder son más rápidos que los de StringBuffer.
  • Los siguientes puntos se aplican a ambas clases (StringBuilder y StringBuffer):
    - Son mutables, pueden cambiar su valor sin crear un nuevo objeto.
    - La invocación de sus métodos cambian el valor del objeto, sin realizar ninguna asignación explícita.
    - El método equals() no está sobreescrito, no compara los valores.
  • La encadenación de métodos se evalúa de izquierda a derecha.
Ver ejemplo: Main01.java

File e I/O (Objetivo 3.2):
Clases que se necesitan saber para el examen:
  • File: Es una representación abstracta de un archivo o directorio. Esta clase no se utiliza para escribir o leer datos, si no para trabajar a alto nivel. Es decir, para crear, buscar, eliminar archivos y para trabajar con directorios y rutas.
  • FileReader: Esta clase se usa para leer caracteres de un archivo. Tiene un método read() de bajo nivel, el cual se puede utilizar para leer un simple carácter, un flujo entero de caracteres, o un número especifico de caracteres.
  • BufferedReader: Esta clase se utiliza para hacer más eficientes las clases de bajo nivel como FileReader. Permite leer grandes cantidades de información de un archivo de una sola vez, y almacenar los datos en un buffer. De este modo cada vez que se necesite obtener la información, se obtendrá desde el buffer minimizando así los accesos a disco. También provee el método readLine().
  • FileWriter: Esta clase se usa para escribir caracteres en un archivo. Posee el método write() que permite escribir caracteres o Strings a un archivo.
  • BufferedWriter: Se utiliza para hacer más eficientes y más fácil de utilizar las clases de bajo nivel como FileWriter. Esta clase escribe grandes cantidades de información a un archivo de una vez, minimizando el número de operaciones de escritura. También provee el método writeLine(), el cual añade los caracteres de salto de línea correspondientes a la plataforma en la que se ejecuta.
  • PrintWriter: Envuelve las clases FileWriter y BufferedWriter. Añade varios métodos como format(), printf() y append() que brindan mayor flexibilidad y poder.
Cuando se crea una instancia de la clase File, solo se está creando el nombre de un archivo. Esta clase contiene métodos de utilidad para realizar operaciones sobre archivos y directorios:
  • public boolean exists(): Devuelve true si el archivo existe.
  • public boolean createNewFile(): Crea un nuevo archivo, si el mismo no existe.
  • public boolean mkdir(): Crea un nuevo directorio, si el mismo no existe.
  • public boolean delete(): Elimina un archivo o directorio. No se puede eliminar directorios si los mismos no se encuentran vacios.
  • public boolean renameTo(): Se pasa como argumento una instancia de File con el nuevo nombre del archivo.
  • public String[] list(): Devuelve un arreglo de String’s con los archivos y directorios contenidos en la ubicación actual.
La clase Console permite aceptar entradas desde la línea de comandos. El método readLine() devuelve un String que contiene lo que el usuario haya ingresado. El método readPassword() devuelve un arreglo de caracteres que contienen el password solicitado. La clase Console no se puede instanciar a través de su constructor. Para obtener una instancia se debe invocar el método estático System.console().

Resumen (TWO-MINUTE DRILL):
  • Las clases que se deben entender del paquete java.io son: File, FileReader, BufferedReader, FileWriter, BufferedWriter, PrintWriter y Console.
  • Cuando se crea un nuevo objeto File, no significa que se crea un nuevo objeto.
  • Un objeto File puede representar tanto un archivo como un directorio.
  • La clase File permite administrar (crear, renombrar y eliminar) archivos y directorios.
  • Los métodos createNewFile() y mkdir() agregan archivos y directorios al sistema de archivos.
  • FileWriter y FileReader son clases I/O de bajo nivel. Se pueden utilizar para escribir y leer archivos, pero generalmente se envuelven en clases con mayor funcionalidad.
  • Es muy común envolver un objeto FileReader con una instancia de BufferedReader o una instancia de FileWriter con una de BufferedWriter, para tener métodos más eficientes y de alto nivel.
  • Los objetos PrintWriter’s pueden ser utilizados para envolver tipos Writer, pero desde Java 5 pueden ser instanciados directamente desde instancias File o String.
  • La clase PrintWriter desde Java 5 posee nuevos métodos: append(), format() y printf().
Ver ejemplo: Main02.java

Serialización (Objetivo 3.3):
La serialización permite guardar el estado de un objeto (es decir, todas sus variables de instancia). Si una variable de instancia tiene el especificador transient, la misma no será almacenada por el proceso de serialización.
Para que un objeto pueda ser serializado, el mismo debe implementar la interface Serializable (dicha interface no posee métodos para implementar, solo es un marcador). Un objeto serializado se almacena en un archivo de bytes, se utilizan dos métodos que permiten leer y escribir un stream de datos.
  • ObjectOutputStream.writeObject()
  • ObjectInputStream.readObject()
Ambas clases son envoltorios de las clases FileOutputStream y FileInputStream. Los métodos writeObject() y readObject() trabajan con objetos de tipo Serializable.

Cuando se intenta serializar un objeto (Serializable) que contiene (relación TIENE-UN) una referencia a otro tipo de objeto pueden ocurrir ciertas situaciones:
  • Si el objeto contenido no implementa la interface Serializable: Se produce la siguiente excepción en tiempo de ejecución NoSerializableException.
  • Si la variable de referencia al objeto contenido está marcada con el especificador transient: Se serializa el estado del objeto contenedor, dejando de lado el estado del objeto contenido.
  • Si la clase del objeto contenido implementa la interface Serializable: Se guarda todo el estado completo del objeto. Es decir, cuando se deserialice el estado del objeto contenedor, también se recuperará el estado del objeto contenido.
El proceso de serialización de Java posee un mecanismo especial para los objetos que son especiales y difíciles de guardar. Se trata de un conjunto de métodos privados que se pueden implementar en la clase en cuestión. Si dichos métodos concuerdan con la signatura exacta, se invocarán durante el proceso de serialización y deserialización del objeto. Las signaturas son las siguientes:
  • private void writeObject (ObjectOutputStream os) { }
  • private void readObject (ObjectInputStream is) { }
Gracias a estos dos métodos es posible manejar el proceso de serialización. Esto se puede realizar debido a que los objetos ‘os’ e ‘is’ tienen un conjunto de métodos para invocar el proceso de serialización por defecto y además métodos para almacenar y leer datos adicionales.
Proceso de serialización por defecto:
  • ObjectOutputStream.defaultWriteObject()
  • ObjectInputStream.defaultReadObject()
Métodos para almacenar y leer datos adicionales:
  • ObjectOutputStream.writeInt()
  • ObjectOutputStream.writeFloat()
  • ObjectInputStream.readInt()
  • ObjectInputStream.readFloat()
La herencia también se ve afectada por el proceso de serialización. Cuando una clase base implementa la interface Serializable, hace que todas sus subclases también la implementen.
El problema real se da cuando una clase base no es Serializable pero una de sus clases hijas si lo es. Esta situación produce que se serialice solo el estado de la clase hija, dejando de lado todas las variables de instancia de la clase base. Para que este escenario sea posible, la clase base debe poseer un constructor por defecto. En caso contrario se producirá un error en tiempo de ejecución a la hora de deserializar el objeto, ya que se intentará invocar automáticamente el constructor por defecto de clase base.

Resumen (TWO-MINUTE DRILL):
  • Las clases que se necesitan conocer del paquete java.io son: ObjectInputStream, ObjectOutputStream, FileInputStream, FileOutputStream.
  • Una clase debe implementar la interface Serializable antes de poder ser serializada.
  • El método ObjectOutputStream.writeObject() serializa los objetos, y el método ObjectInputStream.readObject() deserializa los objetos.
  • Si una variable de instancia es marcada con el especificador transient, la misma no será serializada, aunque todas las demás variables de instancia si lo hagan.
  • Si una superclase implementa la interface Serializable, entonces las subclases lo hacen automáticamente.
  • Si una superclase no implementa Serializable, cuando una subclase se deserializa, se invoca el constructor por defecto de la clase padre.
  • DataInputStream y DataOutputStream no se toman en el examen.
Ver ejemplo: Main03.java

Fechas, números y monedas (Objetivo 3.4):
Clases que se necesitan saber para el examen:
  • java.util.Date: La mayoría de los métodos de esta clase están depreciados. Pero esta clase es útil como puente entre Calendar y DateFormat.
  • java.util.Calendar: Provee una gran variedad de métodos que ayudan a convertir y manipular fechas y tiempos.
  • java.text.DateFormat: Esta clase se utiliza no solo para formatear fechas en varios estilos, sino también para formatear fechas de varias localidades del planeta.
  • java.text.NumberFormat: Esta clase se utiliza para formatear números y monedas para varias localidades del planeta.
  • java.util.Locale: Esta clase se utiliza junto con DateFormat y NumberFormat para formatear fechas, números y monedas para una localidad específica.
Resumen (TWO-MINUTE DRILL):
  • Las clases que se necesitan saber para este objetivo son: java.util.Date, java.util.Calendar, java.text.DateFormat, java.text.NumberFormat y java.util.Locale.
  • La mayoría de los métodos de la clase Date están depreciados.
  • Dentro de una instancia Date, la fecha se almacena como un valor long, la cantidad de milisegundos desde el 1ro de Enero de 1970.
  • Los objetos Date son utilizados con instancias Calendar y Locale.
  • La clase Calendar provee un poderoso conjunto de métodos para manipular fechas, realizar tareas como obtener los días de la semana, o agregar un numero de meses o años (u otros tipos de incrementos) a una fecha.
  • Para crear una instancia de la clase Calendar, se debe invocar el método estático (getInstance()).
  • Los métodos de Calendar que se deben conocer son:
    - add(): Permite adicionar o sustraer varias piezas (minutos, días, horas, etc.) de fechas.
    - roll(): Funciona al igual que add(), a diferencia que no incrementa las piezas más grandes de la fecha.
  • Para crear instancias de la clase DateFormat, se deben utilizar los métodos estáticos getInstance() y getDateInstance().
  • Existen diferentes estilos disponibles en la clase DateFormat.
  • El método DateFormat.format() se utiliza para crear String que contienen fechas formateadas.
  • La clase Locale se utiliza conjuntamente con las clases DateFormat y NumberFormat.
  • Las instancias de DateFormat y NumberFormat pueden ser creadas con un objeto Locale específico, el cuál es inmutable.
  • Para el examen se debe saber cómo crear objetos Locale utilizando el leguaje, o la combinación de lenguaje y país.
Ver ejemplo: Main04.java

Parseo, Tokenizing y Formateo (Objetivo 3.5):
Resumen (TWO-MINUTE DRILL):
  • Las expresiones regulares, son patrones utilizados para buscar información dentro de grandes fuentes de datos.
  • Las expresiones regulares, son un sub-lenguaje que existe dentro de Java.
  • Las expresiones regulares permiten crear patrones de búsqueda mediante caracteres o metacaracteres. Los metacaracteres permiten buscar información más abstracta como dígitos o espacios en blanco.
  • Los metacaracteres utilizados en el examen son: ‘\d’, ‘\w’, ‘\s’ y ‘.’.
  • Los cuantificadores utilizados en el examen son: ‘?’, ‘*’ y ‘+’.
  • Los metacaracteres deben ser escapados dentro de los Strings. String s = “\\d”.
  • Las clases Pattern y Matcher poseen las capacidades más poderosas de Java para las expresiones regulares.
  • Los métodos que son utilizados en el examen son:
    - Pattern: compile()
    - Matcher: matches(), pattern(), find(), start() y group().
  • Tokenizing es el proceso de dividir información delimitada en pequeñas piezas.
  • El proceso Tokenizing se puede llevar a cabo mediante la clase Scanner o a través del método String.split().
  • Los delimitadores pueden ser caracteres simples como una coma, o expresiones regulares.
  • La clase Scanner permite dividir información dentro de un bucle, pero también permite parar este proceso en cualquier parte (a diferencia del método String.split() el cuál debe procesar todos los datos de una vez para obtener un resultado).
  • La clase Scanner permite dividir Strings, streams (flujos de datos) o archivos.
  • A partir de Java 5 se encuentran disponibles los métodos format() y printf(). Los cuales pertenecen a la clase PrintWriter. Ambos métodos poseen la misma funcionalidad, formatear una salida.
  • %[arg_index$][flags][ancho][.precision]carcter de conversión
    - Flags: ‘-‘, ‘+’, ‘0’, ‘,’ y ‘(‘.
    - Conversiones: b, c, d, f y s.
  • Si en carácter de conversión no concuerda con el tipo de argumento, se lanza una excepción.

Control de flujo, Excepciones y Assertions

A continuación dejo el resumen del quinto capítulo (Flow Control, Exceptions, and Assertions) del libro "Sun Certified Programmer for Java 6 Study Guide".

Escribir código que utiliza los bloques if y switch (Objeto 2.1):
  • En un bloque if la única expresión legal es una booleana. Es decir, una expresión que pueda resolverse como un valor boolean o Boolean.
  • Tener cuidado con las asignaciones booleanas (=) ya que pueden ser confundidas con una comprobación de igualdad (==).
  • Los corchetes son opcionales en un bloque if que tenga solo una línea. Tener cuidado con la identación.
  • Un bloque switch solo puede evaluar enumeraciones (enums) o alguno de los tipos primitivos que puedan ser promovidos implícitamente al tipo int (byte, short, int o char).
  • Una constante case debe ser un literal o una variable final, o una expresión constante (incluido un enum). No puede existir una clausula case con una variable que no sea final, o con un rango de valores.
  • Si una condición en un bloque switch coincide con una constante case, la ejecución comenzará a partir de dicha constante case y continuará hasta que se encuentre con una instrucción break, o hasta la finalización del bloque switch. Es decir, la primer constante case que coincida será el punto de de comienzo de la ejecución.
  • La palabra clave default puede ser utilizada en un bloque switch si se quiere ejecutar algún código cuando ninguna de las constantes case aplican.
  • El bloque default puede estar situado en cualquier parte del bloque switch, es decir, antes, después o entre media de constantes case.
Escribir código que utilice bucles (Objetivo 2.2):
  • Un bloque básico for está compuesto por tres partes: declaración y/o inicialización, evaluación booleana, y la expresión de iteración.
  • Si una variable es incrementada o evaluada dentro de un bucle for, aquella debe estar declarada antes del este, o dentro de la declaración del bucle for.
  • Una variable declarada dentro de la sección de declaración de un bucle for solo puede ser utilizada dentro de este. Es decir, el ámbito de la variable es solo dentro del bucle for.
  • Se puede inicializar más de una variable del mismo tipo dentro de la sección de declaración del bucle for; cada inicialización debe estar separada por una coma.
  • Un bloque foreach (introducido en Java 6), está compuesto de dos partes, la declaración y la expresión. Es utilizado solo para iterar a través de los elementos de un arreglo o una colección.
  • En un for (de tipo foreach), la expresión es el arreglo o la colección a través de la cual se va a iterar.
  • En un for (de tipo foreach), la declaración es donde se declara la variable (la cuál es del mismo tipo que los elementos) que contendrá el valor de cada uno de los elementos de la colección.
  • Solo pueden ser utilizados valores o expresiones booleanas dentro de una condición de un if o un bucle. No se pueden utilizar números.
  • En el bucle do, su cuerpo siempre se ejecuta al menos una vez, incluso si la condición no se cumple.
Utilizar break y continue (Objetivo 2.2):
  • La clausula break provoca que la iteración actual de un bucle se detenga y que se ejecute la siguiente línea de código fuera del bucle.
  • La clausula continue provoca que la iteración actual de un bucle se detenga, luego se comprueba la condición del bucle, y en caso de ser afirmativa el bucle se ejecuta nuevamente.
  • Si una clausula break o una continue se encuentran etiquetadas, estas provocan la misma acción pero en el bucle etiquetado, no en el que se está ejecutando.
Manejando excepciones (Objetivos 2.4, 2.4 y 2.6):
  • Existen dos tipos de excepciones: comprobadas y no comprobadas.
  • Las excepciones comprobadas incluyen todos los subtipos de la clase Exception, salvo las subclases de RuntimeException.
  • Cualquier método que pueda lanzar una excepción comprobada la debe declarar en la clausula throws, o tratarla en los correspondientes bloques try/catch.
  • Los subtipos de Error y RuntimeException no son comprobados, por lo tanto el compilador no obliga a tratarlas o declararlas en la clausula throws.
  • Si se utiliza el bloque (opcional) finally, este será ejecutado tanto si el código del bloque try termina correctamente, como si se genera una excepción y la misma es atrapada por un bloque de tratamiento.
  • La única excepción en la que el bloque finally no será ejecutado, es si dentro del bloque try (o algún bloque catch) se realiza una invocación el método System.exit().
  • Que el bloque finally siempre se ejecute, no quiere decir que el mismo se ejecutará completamente. El código dentro de un bloque finally puede lanzar una excepción o invocar el método System.exit().
  • Las excepciones no atrapadas se propagan hacia abajo a través de la pila de llamadas (call stack). Comenzando a partir del método en el que la excepción fue lanzada y terminando en el lugar donde se trate la misma, o finalizando la ejecución del programa.
  • Se pueden declarar nuevas excepciones extendiendo de la clase Exception o alguno de sus subtipos. Estas excepciones son consideradas comprobadas y el compilador obliga a tratarlas o declararlas en la clausula thorws cada vez que sean generadas.
  • Los bloques catch deben estar ordenados desde los tipos de excepciones más específicos hacia los más generales (primero los subtipos y luego las clases bases). En caso contrario, el compilador generará un error porque algún bloque catch nunca va a poder ser alcanzado.
  • Las excepciones pueden ser creadas por la JVM o por un programador.
Trabajando con el mecanismo Assertions (Objetivo 2.3):
  • Una Assertion brinda una manera de testear las suposiciones durante el desarrollo y el debug.
  • Las Assertions típicamente están activadas durante el testeo pero desactivadas durante la implementación.
  • Se puede utilizar ‘assert’ como una palabra clave (a partir de la versión 1.4) o como un identificador, pero nunca de las dos maneras. Para compilar código antiguo que utilice la palabra ‘assert’ como un identificador, se debe usar el comando –source 1.3 a la hora de compilar.
  • El mecanismo Assertions por defecto esta desactivado en tiempo de ejecución. Para activarlo se debe utilizar el comando –ea o –enableassertions.
  • Para desactivar el mecanismo se debe utilizar el comando –da o –disableassertions.
  • Para activar o desactivar el mecanismo Assertions en general, se lo debe hacer sin ningún argumento. Se puede combinar la activación y desactivación para utilizar el alguna clase y/o paquete en especial.
  • No utilizar las assertions para validar argumentos de métodos públicos.
  • No utilizar expresiones assert que provoquen efectos. No se garantiza que las assertions siempre se van a ejecutar, y no se aconseja tener diferentes comportamientos dependiendo de si están o no activadas.
  • Utilizar assertions (incluso en los métodos públicos) para validar bloques de código que nunca van a ser alcanzados. Por ejemplo: assert false; Esto provoca un error en caso de que el código sea ejecutado.

Cadenas de caracteres

Resumen del capítulo 13 (Cadenas de caracteres) del "Libro Thinking in Java (4ta Edición)".

Introducción

Los objetos de clase String son inmutables. Todos los métodos de la clase que parecen modificar una cadena de caracteres, lo que hacen en realidad, es devolver un objeto String completamente nuevo que contiene dicha modificación. Puesto a que este tipo de objetos es inmutable, se pueden establecer tantos alias como se desee, pero no hay ninguna posibilidad de que una referencia modifique algo que pueda afectar a otras referencias.
Los operadores ‘+’ y ‘=+’ para objetos String son los únicos operadores sobrecargados en Java. El compilador cuando se encuentra con alguno de estos operadores (que intentan concatenar Strings), lo que hace es crear un objeto StringBuilder. Con una instancia de esta clase, se invoca el método append() la cantidad de veces necesarias para cada concatenación, y finalmente se invoca el método toString() para devolver el resultado.
Los objetos StringBuilder disponen de un conjunto completo de métodos como: insert(), delete(), replace(), substring(), reverse(), append() y toString().
Ver ejemplo: Main01.java

Clase Formatter

Una de las características incorporadas en Java SE5 es el formateo de la salida al estilo de la instrucción printf() de C. Toda la nueva funcionalidad de formateo de Java es gestionada por la clase Formatter del paquete java.util. Esta clase se puede considerar como una especie de traductor que convierte la cadena de formato y los datos al resultado deseado. Cuando se instancia un objeto Formatter, se le indica a donde se deben mandar los resultados. El constructor de la clase se encuentra sobrecargado para admitir diversas ubicaciones de salida, pero las más útiles son PrintStream, OutputStream y File.
Existe un atajo para realizar el formateo de la salida sin utilizar un objeto Formatter. Todos los objetos PrintStream o PrintWriter (dentro de los que se incluye System.out) tienen disponible el método format().
Para controlar el espaciado y la alineación cuando se insertan los datos, hacen falta especificadores de formato más elaborados:
%[indice_argumento$][indicadores][anchura][.precisión]conversión
  • Índice argumento: Indica el índice del argumento que será utilizado para realizar la conversión.
  • Indicadores: De manera predeterminada, los datos se justifican a la derecha. Para justificar los datos a la izquierda se debe incluir el indicador ‘-‘.
  • Anchura: Permite controlar el tamaño mínimo de un campo. Se garantiza que un campo tenga al menos una anchura de un cierto número de caracteres, rellenándolo con espacios en caso de ser necesario. (Es aplicable a todos los tipos de conversión de datos).
  • Precisión: Se utiliza para especificar un máximo. Tiene un significado distinto para los diferentes tipos:
    - Cadenas de caracteres: Especifica el número máximo de caracteres del objeto String que hay que imprimir.
    - Números en coma flotante: Especifica el numero de posiciones decimales que hay que mostrar (el valor predeterminado el 6), efectuando un redondeo si hay más dígitos o añadiendo más ceros al final si hay pocos.
    - Enteros: La precisión no es aplicable ya que no tienen parte fraccionaría. Se generará una excepción si se utiliza este argumento.
Caracteres de conversión:
  • d: Entero (como decimal)
  • c: Carácter Unicode
  • b: Valor booleanos: Cadena de caracteres
  • f: Coma flotante (como decimal)
  • e: Coma flotantes (en notación científica)
  • x: Entero (como hexadecimal)
  • h: Código hash (como hexadecimal)
Java SE5 también ha tomado prestado de C la idea de sprintf(), que es un método que se utiliza para crear cadenas de caracteres. String.format() es un método estático que toma los mismos argumentos que el método format() de Formatter pero devuelve un objeto String.
Ver ejemplo: Main02.java

Expresiones regulares

Las expresiones regulares son herramientas de procesamiento de texto muy potentes y flexibles. Nos permiten especificar patrones complejos de texto que pueden buscarse en una cadena de entrada. Proporcionan un lenguaje compacto y dinámico que puede emplearse para resolver todo tipo de tareas de procesamiento, comparación, selección, edición y verificación de cadenas de una forma general.
La forma más simple de utilizar las expresiones regulares consiste en utilizar la funcionalidad incluida dentro de la clase String. Esta clase permite realizar búsquedas (matches()), partir la cadena (split()) y realizar sustituciones (replaceFirst() o replaceAll()).
Ver ejemplo: Main03.java

En general, lo que se hace es compilar objetos de expresión regular en lugar de emplear las utilidades String, que son bastante limitadas. Para ello, se debe importar la librería java.util.regex, y luego compilar una expresión regular utilizando el método estático Pattern.compile(). Esto genera un objeto Pattern basado en su argumento String. Para utilizar el objeto Pattern, lo que se hace es invocar el método matcher(), pasándole la cadena de caracteres que se quiere buscar. Este método genera un objeto Matcher, el cual tiene muchas operaciones para trabajar con cadenas.
  • public boolean matches(): Devolverá true solo si el patrón se corresponde con la cadena de entrada completa.
  • public boolean lookingAt(): Devolverá true solo si la cadena de entrada, comenzando por el principio, permite establecer una correspondencia con el patrón.
  • public boolean find(): Puede utilizarse para descubrir múltiples correspondencias de patrón en el objeto CharSequence al cuál se aplique. Es como un iterador, que se desplaza hacia adelantea través de la cadena de caracteres de entrada.
  • public boolean find(int start): El argumento indica en qué posición de la cadena debe comenzar la búsqueda.
  • public String group(): Devuelve la correspondencia completa de la operación anterior de establecimiento de correspondencias (find()).
Ver ejemplo: Main04.java

Clase Scanner

Otra característica agregada en Java SE5 es la clase Scanner. Dicha clase permite eliminar buena parte de la complejidad relacionada con el análisis de una entrada. Esta entrada puede ser tanto un archivo como un flujo de datos. El constructor de Scanner admite casi cualquier tipo de objeto de entrada, File, InputStream, String o un objeto que implemente la interface Readable. Con Scanner, los pasos de entrada, extracción de elementos y análisis sintáctico están implementados mediante diferentes tipos de métodos. El método next() devuelve el siguiente elemento String y existen métodos similares para todos los tipos primitivos (excepto char), como para también para los tipos BigDecimal y BigInteger. También existen los métodos “hasNext” correspondientes que devuelven true si el siguiente elemento de entrada es del tipo correcto.
De manera predeterminada, un objeto Scanner divide los elementos de entrada según los caracteres de espaciados, pero también podemos especificar nuestro patrón delimitador en forma de expresión regular a través del método useDelimiter().
Ver ejemplo: Main05.java

Tratamiento de errores mediante excepciones

Resumen del capítulo 12 (Tratamiento de errores mediante excepciones) del "Libro Thinking in Java (4ta Edición)".

Introducción

Para crear un sistema robusto, cada componente tiene que ser robusto. Al proporcionar un modelo coherente de informe de errores utilizando excepciones, Java permite que los componentes comuniquen los problemas de manera fiable al código cliente. Imponer esta formalidad para el tratamiento de errores permite crear sistemas de gran envergadura utilizando menos código de lo habitual y reducir la complejidad del mismo.
Una excepción se genera cuando ocurre una situación inesperada, lo cual impide continuar con el normal procesamiento, ya que no se posee la información necesaria para tratar con el problema en el contexto actual.
Sucesos que se dan a partir de la generación de una excepción:
  1. Se crea un objeto excepción de la misma manera que cualquier otro objeto (utilizando la instrucción new).
  2. Se detiene la ruta actual de ejecución y se extrae del contexto actual la referencia al objeto excepción.
  3. Luego el mecanismo de tratamiento de excepciones se hace cargo del problema y comienza a buscar un lugar apropiado donde continuar ejecutando el programa.
  4. Dicho lugar es la rutina de tratamiento de excepciones, cuya tarea consiste en recuperarse del problema de modo que el programa pueda intentar hacer otra cosa o simplemente continuar con lo que estuviera haciendo.
Existen tres secciones importantes a la hora de utilizar el mecanismo de excepciones:
  • Región protegida (o bloque try): Este bloque de código es un ámbito de ejecución ordinario, salvo que dentro del mismo se incluye código que podría generar excepciones. Es decir, lo que se hace es “probar” el código (generalmente invocaciones a métodos).
  • Rutina de tratamiento: Cuando se genera una excepción en el bloque try, esta se debe tratar en algún lugar. Este lugar son las rutinas de tratamiento. Generalmente existe una rutina de tratamiento para cada tipo de excepción que pueda ser generada en el bloque try (esto permite actuar en consecuencia de la generación de diversos tipos de errores). Estas rutinas están situadas luego del bloque try y se denotan mediante la palabra clave catch. Solo se ejecutará una sola clausula catch, la que se ajuste al tipo de excepción generada.
  • Finalización (o bloque finally): Es un bloque de código que se ejecuta siempre, más allá de que se haya generado una excepción, o no se haya hecho. Esta sección generalmente se utiliza cuando es necesario restaurar a su estado original alguna otra cosa distinta de la propia memoria. (Es importante recalcar que la ejecución de este bloque de código se realiza SIEMPRE, más allá de que dentro del bloque try haya una instrucción return.)
Ver ejemplo: Main01.java

Excepciones definidas por el usuario

La jerarquía de excepciones de Java no puede prever todos los errores de los que vayamos a querer informar, por eso se pueden crear excepciones definidas por el usuario que indican errores especiales para una biblioteca. Para crear excepciones definidas por el usuario, se debe heredar de una clase de excepción existente, preferiblemente de una cuyo significado esté próximo al de nuestra propia excepción (aunque a menudo esto no es posible). Lo más importante acerca de una excepción es el nombre de la clase, que hace referencia al tipo de error.
Ver ejemplo: codigo/Main02.java

Especificación de excepciones

Mientras se ejecuta un método se puede dar la situación de que se genere una excepción, ante este escenario, se pueden realizar dos cosas. El propio método puede contener una rutina de tratamiento para dicha excepción o que el mismo lance la excepción a un contexto superior. En el segundo caso, la excepción es lanzada al contexto en donde el método fue invocado. Esto significa que la invocación de un método puede generar una excepción (por lo cual la invocación debe estar dentro de un bloque try).
Java proporciona una sintaxis (y es obligatorio utilizarla) para permitir especificar las excepciones que puede generar un método. Se trata de la especificación de excepciones, la cual forma parte de la declaración del método y aparece luego de la lista de argumentos. Utiliza la palabra clave throws y contiene todos los tipos potenciales de excepciones que puede generar el método. El compilador provocará un error en caso de que se intente lanzar una excepción (de tipo comprobada) no declarada en la clausula throws.
NOTA: Las excepciones no comprobadas (excepciones que heredan de la clase RuntimeException o algún subtipo de ella) no se necesitan declarar en la clausula throws. Es obligatorio declarar todas las excepciones comprobadas potenciales que puede lanzar un método.
Ver ejemplo: codigo/Main03.java

La clase Throwable describe todas las cosas que pueden generarse como una excepción. Existen dos tipos generales de objetos Throwable (que heredan de ella). El subtipo Error representa los errores de tiempo de compilación y del sistema de los que no tenemos que preocuparnos de capturar. El subtipo Exception es el tipo básico que puede generarse desde cualquiera de los métodos de la biblioteca estándar de Java.
Existe un conjunto completo de tipos de excepción que son generadas de forma automática por Java y que no es necesario incluir en las especificaciones de excepciones (como se mostro en el ejemplo de código anterior Main03.java). Estas excepciones están agrupadas y son subtipos de la clase RuntimeException. Son denominadas excepciones no comprobadas. Las mismas indican errores de programación, normalmente no se suelen capturar, ya que el sistema las trata automáticamente.

Las excepciones que se necesitan saber para el exámen son:
Derivadas de RuntimeException:
  • ArrayIndexOutOfBoundsException: Excepción que hereda de IndexOutOfBoundsException. Se lanza cuando se intenta acceder a un elemento de un arreglo con un índice inválido o con un valor negativo.
  • ClassCastException: Se lanza cuando se intenta castear una variable de referencia a un tipo que no cumple con la relación ES-UN.
  • IllegalArgumentException: Se lanza cuando un método recibe un argumento formateado diferente a como el método lo espera. Por ej.: el método parseInt() genera esta excepción.
  • IllegalStateException: Se lanza cuando el estado de un ambiente no es el correcto para la operación que se esta intentando. Por ej.: utilizar un objeto Scanner que esta cerrado.
  • NullPointerException: Se lanza cuando se intenta acceder a un objeto con una variable de referencia con valor null.
  • NumberFormatException: Excepción que hereda de IllegalArgumentException. Se lanza cuando un método que convierte un String a un número, recibe un String que no puede ser convertido.
Derivadas de Error:
  • AssertionError: Se lanza cuando cuando la expresión dentro de un assert tiene el resultado false.
  • ExceptionInInitializerError: Hereda de LinkageError. Se lanza para indicar que ha ocurrido una excepción al momento de inicializar una variable estática o un bloque de inicialización estático.
  • StackOverflowError: Hereda de VirtualMachineError. Se lanza cuando se produce un desbordamiento de la pila. Esta situación se da debido a un método que recursa profundamente.
  • NoClassDefFoundError: Hereda de LinkageError. Se lanza la JVM no puede encontrar la definición de una clase que necesita. Sus causas pueden ser: un error al especificar la clase en la línea de comandos, un tema relacionado al classpath, o la falta del archivo .class.

Restricciones de la excepciones

Cuando sustituimos un método, sólo podemos generar aquellas excepciones que hayan sido especificadas en la versión del método correspondiente a la clase base. Esta restricción implica que el código que funcione con la clase base funcionará también automáticamente con cualquier objeto derivado de la clase base.
En cambio los constructores pueden generar todas aquellas excepciones que deseen, independientemente de lo que genere el constructor de la clase base. Sin embargo, puesto que siempre hay que invocar algún constructor de la clase base, el constructor de la clase derivada deberá declarar todas las excepciones del constructor de la clase base en su propia especificación de excepciones.
En resumen, la interfaz de especificación de excepciones de un método concreto puede estrecharse durante la herencia y cuando se realizan sustituciones, pero nunca ensancharse (solo puede ensancharse con excepciones derivadas a la declarada en el método de la clase base ver ejemplo). En cambio en un constructor de una clase derivada, la interfaz de especificación de excepciones puede ensancharse pero no estrecharse.
Ver ejemplo: codigo/Main04.java

Almacenamiento de objetos (Contenedores)

Resumen del capítulo 11 (Almacenamiento de objetos) del "Libro Thinking in Java (4ta Edición)".

Introducción

La biblioteca java.util posee un conjunto bastante completo de clases contenedoras. Existen cuatro tipo básicos: List, Set, Queue y Map (lista, conjunto, cola y mapa). Los contenedores proporcionan formas más sofisticadas (que las matrices) de almacenar objetos.
La biblioteca de contenedores Java toma la idea de “almacenamiento de los objetos” y la divide en dos conceptos distintos, expresados mediante las interfaces básicas de la biblioteca:
  • Collection: una secuencia de elementos individuales a los que se aplica una o más reglas.
    - List: almacena los elementos en la forma en la que fueron insertados.
    - Set: no puede tener elementos duplicados.
    - Queue: produce los elementos en el orden determinado por una disciplina de cola.
  • Map: un grupo de parejas de objetos clave-valor, que permite efectuar búsquedas de valores utilizando una clase. Un mapa permite buscar un objeto utilizando otro objeto. También se lo denomina matriz asociativa o diccionario.
Estas interfaces utilizan tipos genéricos, es decir, se debe especificar el tipo de objeto/s que almacenará el contenedor. Esto se realiza a través de corchetes angulares que rodean los parámetros de tipo.
De ser posible, se debe intentar escribir la mayor parte del código para que se comunique con estas interfaces. El único lugar donde se debería especificar el tipo concreto del contenedor es en la creación del mismo.
Ver ejemplo: Main01.java

Existen métodos de utilidad en las clases Arrays y Collections de java.util que añaden grupos de elementos a una colección:
  • Collections.addAll (Collection c, T… elements): toma un objeto de tipo Collection como primer argumento y una lista de valores separados por coma. Añade los elementos a la colección.
  • Arrays.asList (T… a): toma una lista de valores separados por coma y lo transforma en un objeto List.
Ver ejemplo: Main02.java

List

Las listas garantizan que los elementos se mantengan en una secuencia concreta. La interface List añade varios métodos a Collection que permiten la inserción y la eliminación de elementos en mitad de una lista.
Existen dos tipos de objetos List:
  • ArrayList: Es el que mejor permite acceder a los elementos de forma aleatoria, pero que resulta más lento a la hora de insertar y eliminar elementos en mitad de una lista.
  • LinkedList: Proporciona un acceso secuencial óptimo, siendo las inserciones y borrados en mitad de una lista enormemente rápidos. Resulta relativamente lento para los accesos aleatorios, pero tiene muchas más funcionalidades que ArrayList.
Métodos de la interface:
  • boolean contains(Object o): Permite averiguar si un objeto se encuentra dentro de la lista.
  • boolean remove(Object o): Permite eliminar un objeto, pasando la referencia al mismo.
  • int indexOf(Object o): Pasando como parámetro una referencia a un objeto, devuelve el número de índice en el que ese objeto está almacenado.
Estos últimos tres métodos utilizan el método equals() para realizar sus operaciones. Es decir, sobrescribiendo este método (perteneciente a la clase Object) se puede alterar el funcionamiento de estas operaciones.
  • List subList(int fromIndex,int toIndex): Permite crear una sublista (respaldada) a partir de otra lista de mayor tamaño. Los cambios efectuados en la lista devuelta se verán reflejados en la lista original, y viceversa.
  • boolean containsAll(Collection c): Verifica si los elementos pertenecen a la lista.
  • boolean retainAll(Collection c): Es una operación de intersección de conjuntos, el comportamiento resultante depende de la implementación del método equals().
  • boolean removeAll(Collection c): Elimina de la lista todos los objetos que estén en el argumento de tipo List. También opera de manera distinta dependiendo del método equals().
  • boolean isEmpty(): Devuelve true si la lista se encuentra vacía.
  • void clear(): Elimina todos los elementos de la lista.
  • E set(int index, E element): Se encarga de sustituir el elemento situado en el índice indicado (el primer argumento) con el segundo argumento.
Ver ejemplo: Main03.java

Set

Los objetos de tipo Set (conjuntos) no permiten almacenar más de una instancia de cada objeto, impide la duplicación. Para insertar un nuevo elemento se debe verificar la no pertenencia, por este motivo la operación más importante de un conjunto suele ser el de la búsqueda. Set tiene la misma interfaz que Collection, por lo que no existe ninguna funcionalidad adicional.
Existen tres tipos de contenedores Set:
  • HashSet: Utiliza el mecanismo de hash para acelerar las búsquedas de elementos.
  • TreeSet: Mantiene los elementos ordenados en una estructura de datos de tipo árbol rojo-negro. Este objeto es útil para mantener los objetos ordenados.
  • LinkedHashSet: Es un subtipo de HashSet, por lo tanto también emplea una función hash para acelerar las búsquedas, pero mantiene los elementos en orden de inserción utilizando una lista enlazada.
Ver ejemplo: Main04.java

Queue

Una cola es normalmente un contenedor de tipo FIFO (first-in, first-out). El orden en el que se introduzcan los elementos coincidirá con el orden en que estos serán extraídos. Las colas son especialmente importantes en la programación concurrente.
Existen dos tipos de contenedores Queue:
  • LinkedList: Dispone de métodos para soportar el comportamiento de una cola e implementa la interface Queue, por lo que un objeto LinkedList puede utilizarse como implementación de Queue.
  • PriorityQueue: Es una implementación automática de una cola con prioridad. Este tipo de contenedor implica que el elemento que va a continuación será aquel que tenga una necesidad mayor (la prioridad más alta). El mecanismo de ordenación predeterminado utiliza el orden natural de los objetos de la cola, pero se puede modificar dicho elemento proporcionando otro objeto Comparator.
Arroja una excepción Retorna un valor especial
Inserta boolean add(E e) boolean offer(E e)
Elimina E remove() E poll()
Obtiene E element() E peek()

Ver ejemplo: Main05.java

Map

Los mapas permiten asociar objetos con otros objetos. Es decir, almacenan parejas de clave-valor. En los contenedores no se pueden almacenar primitivas, por eso la característica autoboxing convierte las primitivas en sus correspondientes objetos envoltorios. Este tipo de contenedores puede expandirse fácilmente para que sean multidimensionales, basta con definir un objeto Map cuyos valores sean también mapas.
Existen tres tipos de contenedores Map:
  • HashMap: Están diseñados para un acceso rápido.
  • TreeMap: Mantiene sus claves ordenadas y no es tan rápido como el anterior.
  • LinkedHashMap: Mantiene sus elementos en orden de inserción, pero proporciona un acceso rápido con mecanismos de hash.
Métodos de la interface:
Ver ejemplo: Main06.java

Iteradores

Iterator
Es una interface que permite desplazarse a través de una secuencia y seleccionar cada uno de los objetos que la componen.
  • boolean hasNext(): Permite ver si hay más objetos en la secuencia.
  • E next(): Obtiene el siguiente objeto de la secuencia.
  • void remove(): Elimina el último elemento devuelto por el iterador.
ListIterator
Es un subtipo más potente de Iterator que solo se genera mediante las clases List. ListIterator es bidireccional, puede generar los índices de los elementos siguiente y anterior, en relación al lugar de la lista hacia el que el iterador está apuntando y también posee otras funcionalidades. Los métodos que adiciona esta interface son:
  • boolean hasPrevious(): Realiza la misma acción que hasNext() pero en sentido contrario.
  • E previous(): Idem next() pero en sentido contrario.
  • int nextIndex(): Obtiene el número de índice del elemento siguiente.
  • int previousIndex(): Obtiene el número de índice del elemento anterior.
  • void set(E e): Permite sustituir el elemento al que apunta por el iterador.
  • void add(E e): Inserta un elemento en la lista.
NOTA: NO HAY NECESIDAD DE UTILIZAR LAS CLASES ANTIGUAS Vector, Hashtable y Stack.

Clases Internas

Resumen del capítulo 10 (Clases Internas) del "Libro Thinking in Java (4ta Edición)".

Introducción

Las clases internas nos permiten agrupar clases relacionadas y controlar la visibilidad mutua de esas clases. Un objeto de una clase interna conoce todos los detalles de la instancia de su clase contenedora y puede comunicarse con ella. Esto se logra debido a que la instancia de la clase interna dispone de un enlace al objeto contenedor que la ha creado (de este modo se puede acceder a todos los miembros del objeto contenedor). Solo se puede instanciar una clase interna a través de una referencia al objeto de la clase contenedora.
Ver ejemplo: Main01.java

Desde una instancia de una clase interna se puede referenciar al objeto contenedor de la siguiente manera: NombreClaseContenedra.this
Como se puede ver en el ejemplo Main01.java, no es posible crear un objeto de la clase interna a menos que ya se disponga de un objeto de la clase externa (o contenedora). Esto se debe a que el objeto de la clase interna se conecta de manera transparente al de la clase externa que lo haya creado. (Esto resuelve también las cuestiones relativas a los ámbitos de los nombres en la clase interna)
Las clases normales (no internas) no pueden ser privadas o protegidas (solo pueden tener acceso público o de paquete). Las clases internas pueden tener cualquiera de los cuatro tipos de acceso. Esto permite ocultar las implementaciones de las clases internas y evitar las dependencias de la codificación de tipos. Main02.java: Por defecto los métodos de una interface son públicos, a través de la implementación (de dicha interface) de una clase interna privada (o protegida) los mismos pueden ser no visibles y no estar disponibles.
Ver ejemplo: Main02.java

Clases internas locales

Las clases internas pueden crearse dentro de un método o incluso dentro de un ámbito arbitrario. Estas clases (definidas dentro de métodos) se denominan clases internas locales. Main03.java: Las clases definidas dentro de bloques if – else, se compilan con todo el resto del código. Sin embargo estas clases no están disponibles fuera del ámbito en el que se definen, por lo demás se asemejan a clases normales.
Ver ejemplo: Main03.java

Clases internas anónimas

Una clase interna anónima, es una clase que no tiene nombre. Generalmente se utilizan para implementar una interface (o extender de otra clase) y devolverse a través de un método. Es decir, se crean solo con el objetivo de darle una implementación a una interface. Este tipo de clases están en cierta forma limitadas si se comparan con el mecanismo normal de herencia, porque pueden extender una clase o implementar solo una interface, pero no pueden hacer ambas cosas al mismo tiempo.
Luego de definir una clase interna anónima se cierra la expresión con un punto y coma. Si se está definiendo una clase interna anónima y se quiere usar un objeto que este definido fuera de la clase interna anónima, el compilador requiere que la referencia al argumento sea FINAL. Si esto no se hace el compilador generará un mensaje de error.
Ver ejemplo: Main04.java

Clases anidadas (Clases internas estáticas)

Una clase anidada es una clase interna estática. Este tipo de clases internas son útiles cuando no es necesario disponer de una conexión entre el objeto de la clase interna y el objeto de la clase externa. No se puede acceder a un objeto no estático de la clase externa desde un objeto de una clase anidada (debido a que no existe una conexión con la clase externa).
A diferencia de las clases internas normales que no pueden tener miembros estáticos o clases anidadas, las clases anidadas si pueden contener estos elementos.
Ver ejemplo: Main05.java

Ejemplos de código - Operadores

A continuación dejo unos ejemplos de código con explicaciones. Estos ejemplos surgieron a partir de la realización de los ejercicios del capítulo 4 (Operators) del libro "Sun Certified Programmer for Java 6 Study Guide".
Para ver el post con el resumen de dicho capítulo haz click aquí.
Cada ejemplo tiene un link para visualizar el código original desde el proyecto (SVN) google.

EJEMPLO 1 (Ver código original)

01. public class Feline {
02. public static void main(String[] args) {
03. Long x = 42L;
04. Long y = 44L;
05. System.out.print(" " + 7 + 2 + " ");
06. System.out.print(foo() + x + 5 + " ");
07. System.out.print(x + y + foo());
08. }
09. static String foo() { return "foo"; }
10. }
/*
SALIDA:
72 foo425 86foo
*/

Las concatenaciones se ejecutan de izquierda a derecha. Se pueden realizar concatenaciones entre diferentes tipos de operandos, es decir, se pueden concatenar tipos primitivos con objetos o viceversa (una concatenación de un objeto se traduce en el llamado al método toString(), para ver una explicación más detallada de la concatenación de objetos ver el ejemplo ConcatenacionObejtos.java).
En resumen, si el primer operando de una concatenación es un String se tomarán todos los siguientes como cadenas (línea 5 y 6). En caso contrario, se realizarán todas las operaciones de adición hasta que aparezca la primer cadena de caracteres, la cuál provocará que se tome todos los demás valores como cadenas (línea 7).

EJEMPLO 2 (Ver código original)

01. public class SpecialOps {
02. public static void main(String[] args) {
03. String s = "";
04. Boolean b1 = true;
05. Boolean b2 = false;
06. if((b2 = false) | (21%5) > 2) s += "x";
07. if(b1 || (b2 = true)) s += "y";
08. if(b2 == true) s += "z";
09. System.out.println(s);
10. }
11. }
/*
SALIDA:
y
*/

En la línea 6 la primera expresión es una asignación (b2 = false). El resultado de cualquier expresión de asignación es el valor de la variable después de la asignación. La sustitución del operador == por = funciona solo con las variables booleanas (debido a que el bloque if solo puede comprobar expresiones booleanas).

En el caso de una variable de tipo int, por ejemplo:
int x = 0;
if (x = 2) {}
El código anterior daría un error de compilación, ya que el resultado de la asignación sería el valor 2. Un valor entero no puede ser utilizado donde se necesita un valor booleano.