Fundamentos de programación: Java
Full text
Miguel Toro Bonilla FUNDAMENTOS DE PROGRAMACIÓN: JAVA Cubierta
Fundamentos de programación: JAVA Fundamentos de programación: JAVA Miguel Toro Bonilla Portadilla
Fundamentos de programación: JAVA Miguel Toro Bonilla Página de la colección Colección: Manuales de Informática del Instituto de Ingeniería Informática Director de la Colección Miguel Toro Bonilla. Universidad de Sevilla Consejo de Redacción Miguel Toro Bonilla. Universidad de Sevilla Mariano González Romano. Universidad de Sevilla Andrés Jiménez Ramírez. Universidad de Sevilla Comité Científico Antón Cívit Ballcells. Universidad de Sevilla María José Escalona Cuaresma. Universidad de Sevilla Francisco Herrera Triguero. Universidad de Granada Carlos León de Mora. Universidad de Sevilla Alejandro Linares Barranco. Universidad de Sevilla Mario Pérez Jiménez. Universidad de Sevilla Mario Piattini. Universidad de Castilla-La Mancha Ernesto Pimentel. Universidad de Málaga José Riquelme Santos. Universidad de Sevilla Agustín Risco Núñez. Universidad de Sevilla Nieves Rodríguez Brisaboa. Universidad de Castilla-La Mancha Antonio Ruiz Cortés. Universidad de Sevilla José Luis Sevillano Ramos. Universidad de Sevilla Ernest Teniente. Universidad Politécnica de Cataluña Francisco Tirado Fernádez. Universidad Complutense de Madrid
Miguel Toro Bonilla Fundamentos de programación: JAVA Sevilla 2022 Fundamentos de programación: JAVA Miguel Toro Bonilla Portada
Fundamentos de programación: JAVA Miguel Toro Bonilla Página de créditos Colección: Manuales de Informática del Instituto de Ingeniería Informática (I3US) Núm.: 1 Editorial Universidad de Sevilla 2022 c/ Porvenir, 27 - 41013 Sevilla. Tlfs.: 954 487 447; 954 487 451; Fax: 954 487 443 Correo electrónico: [email protected] Web: https://editorial.us.es Miguel Toro 2022 DOI: https://dx.doi.org/10.12795/9788447223596 Maquetación: Miguel Toro Diseño de cubierta y edición electrónica: refer[email protected] Comité editorial: Araceli López Serena (Directora de la Editorial Universidad de Sevilla) Elena Leal Abad (Subdirectora) Concepción Barrero Rodríguez Rafael Fernández Chacón María Gracia García Martín Ana Ilundáin Larrañeta María del Pópulo Pablo-Romero Gil-Delgado Manuel Padilla Cruz Marta Palenque Sánchez María Eugenia Petit-Breuilh Sepúlveda José-Leonardo Ruiz Sánchez Antonio Tejedor Cabrera Esta obra se distribuye con la licencia Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional (CC BY-NC-SA 4.0)
7 Agradecimientos ara aprender a programar lo mejor es programar. En esta asignatura de Fundamentos de Programación vamos a aprender los conceptos básicos de la programación. Estos conceptos los vamos a concretar en dos lenguajes: Python y Java. En este volumen veremos Java. Los lenguajes de programación tienden a ir compartiendo las mismas ideas básicas. Cada lenguaje va tomando prestadas las ideas más novedosas aportadas por otros. Aunque cada lenguaje tiene sus particularidades, podemos decir que los lenguajes de programación van convergiendo hacia elementos comunes a todos ellos. Por ello, parece sensato aprender las ideas comunes a los principales lenguajes actuales y extrapolar ideas y conceptos de unos a otros. Vamos a abordar conceptos en Java comunes con Python que se pueden extender a otros. En Java hay dos estilos de programación que queremos aprender: el estilo funcional usando streams y el más clásico estilo imperativo usando iterables. El diseño de tipos ocupará un lugar central en este material. Al final se incluyen varios ejemplos que parten de un diseño de tipos. El material en este texto procede de la experiencia de varios años de enseñanza de la asignatura de Fundamentos de Programación en la Universidad de Sevilla. P Fundamentos de programación: JAVA Miguel Toro Bonilla Agradecimientos
Agradecimientos 8 Mucho de este material está tomado de versiones anteriores de los profesores Mariano González, Fermín Cruz y Pepe Riquelme, a los que quiero agradecer sus esfuerzos y dedicación. Estas versiones anteriores han sido transformadas y actualizadas hasta alcanzar la forma actual. Sus defectos son responsabilidad única del autor. En https://github.com/migueltoro/java_v1 puede encontrarse el código de los ejemplos. Futuras versiones aparecerán en https://github.com/migueltoro/java_v* Miguel Toro Sevilla, septiembre de 2021 Fundamentos de programación: JAVA Miguel Toro Bonilla
9 Índice AGRADECIMIENTOS 7 ÍNDICE 9 CONCEPTOS BÁSICOS DE LA P.O.O. 14 OBJETOS 16 CLASES, REGISTROS E INTERFACES 17 ESTRUCTURA DE UN PROGRAMA EN JAVA 21 ELEMENTOS BÁSICOS DEL LENGUAJE 23 IDENTIFICADORES 23 PALABRAS RESERVADAS DE JAVA 24 LITERALES 25 COMENTARIOS 25 TIPOS DE DATOS 26 TIPOS PROPORCIONADOS POR JAVA 26 VARIABLES Y CONSTANTES 28 VARIABLES 28 CONSTANTES 29 EXPRESIONES Y OPERADORES 30 EXPRESIONES 30 Fundamentos de programación: JAVA Miguel Toro Bonilla Índice
Conceptos básicos de la P.O.O. 16 La POO (Programación Orientada a Objetos) es una forma de construir programas de ordenador donde las entidades principales son los objetos. Está basada en la forma que tenemos los humanos de concebir objetos, distinguir unos de otros mediante sus propiedades y asignarles funciones o capacidades. Estas dependerán de las propiedades que sean relevantes para el problema que se quiere resolver. Los elementos básicos de la POO son: • Objeto • Interfaz • Clase o Atributos (almacenan las propiedades) o Métodos (consultan o actualizan las propiedades) • Record: Es un tipo específico de clase • Paquete Objetos Los objetos tienen una identidad, unas propiedades, un estado y una funcionalidad asociada: • Cada objeto tiene una identidad que lo hace único y lo distingue del resto de objetos del mismo tipo. Puede haber varios objetos con el mismo estado pero cada uno con su identidad propia; en ese caso se dice que los objetos son iguales, pero no idénticos. • Las propiedades son las características observables de un objeto desde el exterior de este. Pueden ser de diversos tipos (números enteros, reales, textos, booleanos, etc.). • El estado indica cuál es el valor de sus propiedades en un momento dado. • La funcionalidad de un objeto se ofrece a través de un conjunto de métodos. Los métodos actúan sobre el estado del objeto (pueden consultar o modificar las propiedades) y son el mecanismo de comunicación del objeto con el exterior. Fundamentos de programación: JAVA Miguel Toro Bonilla Objetos
Conceptos básicos de la P.O.O. 17 La encapsulación es un concepto clave en la POO y consiste en ocultar la forma en que se almacena la información que determina el estado del objeto. Esto conlleva la obligación de que toda la interacción con el objeto se haga a través de ciertos métodos implementados con ese propósito (se trata de ocultar información irrelevante para quien utiliza el objeto). Las propiedades de un objeto sólo serán accesibles para consulta o modificación a través de sus métodos. La encapsulación permite definir de forma estricta las responsabilidades de cada objeto, lo que facilita la depuración de las aplicaciones cuando se detectan errores. Clases, registros e interfaces Las clases son las unidades de la POO que permiten definir los detalles del estado interno de un objeto (mediante los atributos), obtener las propiedades de los objetos a partir de los atributos e implementar las funcionalidades ofrecidas por los objetos (a través de los métodos). Cada clase define un tipo nuevo con el cual podemos declarar variables de ese tipo y construir objetos. Normalmente para implementar una clase partimos de un diseño del tipo. Al diseñar un tipo nuevo debemos partir de los ya existentes. Es necesario decidir a qué otros tipos extender o que tipos usar. Decimos que el nuevo tipo usa aquellos tipos con los que declara sus propiedades. También puede diseñarse el nuevo tipo extendiendo algunos de los disponibles. Un tipo extiende a otro cuando le añade nueva funcionalidad es decir nuevas propiedades y métodos. Al diseñar un tipo estamos estableciendo un contrato entre los objetos de este tipo y sus posibles usuarios. Un tipo tiene un nombre y un conjunto de propiedades que son las características visibles. Cada propiedad tiene un nombre, un tipo, puede ser consultada y además modificada o sólo consultada, y puede ser una propiedad básica o una propiedad derivada. Además, las propiedades pueden ser individuales y compartidas. Las propiedades individuales son específicas de un objeto individual. Las propiedades compartidas son comunes a todos los objetos de la población del tipo. Las propiedades derivadas pueden ser calculadas a partir de las Fundamentos de programación: JAVA Miguel Toro Bonilla Clases, registros e interfaces
Conceptos básicos de la P.O.O. 18 otras propiedades. Las básicas no. Los tipos inmutables solo tienen propiedades modificables. Un tipo suele tener asociado un criterio de igualdad entre dos de ellos y en muchos casos un criterio de ordenación al que llamaremos orden natural. Para un tipo también podemos definir su representación. Es decir la forma en la que se representará el objeto mediante una cadena de caracteres. Un tipo, también, tiene métodos de factoría. Es decir mecanismos para crear objetos nuevos. Entre los métodos de factoría hay uno, que denominaremos parse, encargado de construir objetos a partir de una cadena de texto. Un tipo puede ser diseñado para que sea mutable o inmutable. Un tipo inmutable no tiene operaciones para cambiar sus propiedades. Una vez creado el objeto no puede cambiarlas. Un tipo mutable sí. Nuestros tipos serán generalmente inmutables. Un tipo nuevo en Java se implementa mediante una clase (class). Java nos ofrece una forma simple de definir tipos nuevos mediante tipo de especial de clase llamado record. En un primer momento usaremos records para definir tipos nuevos y clases para agregar un conjunto de funciones que definen una funcionalidad. En Java no podemos definir funciones fuera de una clase. Las propiedades y la funcionalidad decidida en el diseño del tipo se concretan en unos atributos que especifican el estado de los objetos del tipo y en un conjunto de métodos de la clase que gestionan esos atributos. Cada método tiene una signatura que incluye su nombre, el tipo de los parámetros que recibe y el tipo que devuelve en su caso. Llamaremos a este tipo devuelto tipo de retorno. A las signaturas de un método las llamaremos también cabecera del método o prototipo. Además de la cabecera el método tiene un cuerpo formado por una secuencia de sentencias. Un diseño preliminar del tipo Punto2D, un punto en el espacio de dos dimensiones es: Fundamentos de programación: JAVA Miguel Toro Bonilla
Conceptos básicos de la P.O.O. 19 Punto2D Propiedades x: Double, básica y: Double, básica distanciaAlOrigen: Double, derivada, �𝑥𝑥𝑥𝑥2+𝑦𝑦𝑦𝑦2 distancia_a(p:Punto2D): Double, derivada,�(𝑥𝑥𝑥𝑥−𝑝𝑝𝑝𝑝.𝑥𝑥𝑥𝑥)2+ (−𝑝𝑝𝑝𝑝.𝑦𝑦𝑦𝑦)2 cuadrante: Cuadrante, derivada Representación (2.3,4.5) Orden Natural Según su distancia al origen Factoría of(Double x,Double): Punto2D parse(text:String):Punto2D La implementación en Java es recordPunto2D(Double x,Double y)implements Comparable<Punto2D>{ public static Punto2D of(Double x, Double y) { return new Punto2D(x, y); } public Double distanciaAlOrigen() { Double dx = this.x; Double dy = this.y; return Math.sqrt(dx*dx+dy*dy); } @Override public String toString() { return String.format( "(%.2f,%.2f)",this.x(),this.y()); } @Override public int compareTo(Punto2D p) { return this.distanciaAlOrigen() .compareTo(p.distanciaAlOrigen()); } } Una interface es un elemento de la POO que permite, establecer propiedades y funcionalidades que debe tener un tipo dado. Una interface Fundamentos de programación: JAVA Miguel Toro Bonilla
Conceptos básicos de la P.O.O. 20 contiene, fundamentalmente, signaturas de métodos que definen la funcionalidad pero no el cuerpo de estos. Al igual que una clase una interfaz define un tipo nuevo. Una interface se usa para establecer funcionalidades comunes a varios tipos. Como ejemplo veamos la interface Comparable<E> que define la funcionalidad de todos los tipos que tengan un orden natural. interface Comparable<E> { int compareTo(E other); } Un tipo E que ofrezca esta funcionalidad asegura que si un objeto o1 de ese tipo se compara con otro o2 podremos saber si es mayor, menor o igual que este. Esto se concreta en la sentencia: int r = o1.compareTo(o2); La variable r tendrá un número negativo, positivo o cero según que o1 sea menor, mayor o igual a o2. Si un tipo, implementado como class o record, ofrece la funcionalidad definida en una interface decimos que el tipo la implementa. Esto se concreta mediante implements y obliga a la clase o record a dar un cuerpo al o lo métodos contenidos en la interface. record Punto2D(Double x,Double y) implements Comparable<Punto2D> Las propiedades básicas del tipo, las vienen como parámetros de record, x, y en el caso del Punto2D definen el estado del objeto. El estado es privado e invisible fuera del objeto. Para consultar el estado disponemos de métodos. En el caso de Punto2D disponemos de los métodos x(), y(), distanciaAlOrigen(), toString(), compareTo(Punto2D other). Mediante el operador punto (.) podemos combinar objetos y métodos para formar expresiones bien formadas. Los métodos de factoría no se combinan con objetos sino con el nombre del tipo. Fundamentos de programación: JAVA Miguel Toro Bonilla
Conceptos básicos de la P.O.O. 21 Siendo p1, p2 objetos del tipo Punto2D tenemos las expresiones siguientes. Unas bien bien formadas y otras no: Punto2D p1 = Punto2D.of(3.,4.); Double r = p1.x(); Double r2 = p1.x; //Mal, x es invisible p1.y = 3.4; //Mal, y es invisible p1.y() = 5.6; //Mal, la llamada a un método no puede estar a // la izquierda Las expresiones, cuando están bien formadas, tienen un tipo y un valor. El punto (.) es, por lo tanto, un operador que combina un objeto con sus métodos y que indica que el correspondiente método se ha invocado sobre el objeto. Como hemos dicho antes el operador punto (.) también combina el nombre de una clase o registro con un método de factoría. Estructura de un programa en Java Un programa en Java está formado por un conjunto de declaraciones de tipos, interfaces y clases. Un programa puede estar en dos modos distintos. En el modo de compilación está cuando estamos escribiendo las clases e interfaces. En este modo, a medida que vamos escribiendo, el entorno va detectado si las expresiones que escribimos están bien formadas. Si el entorno no detecta errores entonces diremos que el programa ha compilado bien y por lo tanto está listo para ser ejecutado. Se llama modo de compilación porque el encargado de detectar los errores en nuestros programas, además de preparar el programa para poder ser ejecutado, es otro programa que denominamos comúnmente compilador. En el modo de ejecución se está cuando queremos obtener los resultados de un programa que hemos escrito previamente. Decimos que ejecutamos el programa. Para poder ejecutar un programa este debe haber compilado con éxito previamente y tener un método especial denominado método principal (también denominado programa principal). En otro caso no lo podremos ejecutar. En el modo de ejecución pueden aparecer nuevos errores. Los errores que pueden ser detectados en el modo de compilación y los que se detectan en el modo de ejecución son diferentes. Hablaremos de ellos más Fundamentos de programación: JAVA Miguel Toro Bonilla Estructura de un programa en Java
Conceptos básicos de la P.O.O. 22 adelante. Cuando queremos ejecutar un programa en Java éste empieza a funcionar en la clase concreta elegida de entre las que tengan un método de nombre main. Es decir, un programa Java empieza a ejecutarse por el método main de una clase seleccionada. Las interfaces y clases diseñadas más arriba forman un programa. Necesitamos una que tenga un método main. En el ejemplo siguiente se presenta una que sirve para comprobar el buen funcionamiento del tipo Punto. package test; public class TestPunto { public static void main(String[] args) { Punto2D p = Punto2D.of(2.0, 3.0); System.out.println(String.format( "Punto: %s" + p)); } } El programa empieza a ejecutarse en el método main de la clase seleccionada que en este caso es TestPunto. Esta ejecución sólo puede llevarse a cabo cuando han sido eliminados todos los posibles errores en tiempo de compilación y por lo tanto las expresiones están bien formadas. En la línea Punto2D p = Punto2D.of(2.0, 3.0); Se declara p como un objeto de tipo Punto2D y se inicializa con un objeto nuevo en el estado (2.0, 3.0), mediante un método de factoría. Las instrucciones System.out.println(…) permiten mostrar en pantalla las cadenas de texto que se pasan como parámetros entre los paréntesis (se verá más adelante en este tema). Mediante String.format(formato,p1, p2,…) se construye una cadena de caracteres con el formato dado a partir de los parámetros p1, p2, … Los resultados esperados en la consola son: Punto: (2.0,3.0) Fundamentos de programación: JAVA Miguel Toro Bonilla
23 Elementos básicos del lenguaje Identificadores Son palabras que permiten referenciar los diversos elementos que constituyen el código. Es decir, sirven para nombrar a clases, interfaces, métodos, atributos, variables, parámetros y paquetes. Para nombrar a estos elementos, hay que seguir unas determinadas reglas para que puedan ser entendidos por el compilador. Los identificadores se construyen mediante una secuencia de letras, dígitos, o los símbolos _ y $. En cualquier caso, se debe observar que: • No pueden coincidir con palabras reservadas de Java (ver más adelante) • Deben comenzar por una letra, _ o $, aunque estos dos últimos no son aconsejables. • Pueden tener cualquier longitud. • Son sensibles a las mayúsculas, por ejemplo, el identificador min es distinto de MIN o de Min. Ejemplos de identificadores válidos son los siguientes: tiempo, distancia1, caso_A, PI, velocidad_de_la_luz Por el contrario, los siguientes nombres no son válidos (¿por qué?) Fundamentos de programación: JAVA Miguel Toro Bonilla Elementos básicos del lenguaje Identificadores
Elementos básicos del lenguaje 24 1_valor, tiempo-total, dolares%, final En general, es muy aconsejable elegir los nombres de los identificadores de forma que permitan conocer a simple vista qué representan, utilizando para ello tantos caracteres como sean necesarios. Esto simplifica enormemente la tarea de programación y –sobre todo– de corrección y mantenimiento de los programas. Es cierto que los nombres largos son más laboriosos de teclear, pero, en general, resulta rentable tomarse esa pequeña molestia. Unas reglas aconsejables para los identificadores son las siguientes: • Las variables normalmente tendrán nombres de sustantivos y se escribirán con minúsculas, salvo cuando estén formadas por dos o más palabras, en cuyo caso, el primer carácter de cada palabra se escribirá en mayúscula. Por ejemplo: salario, salarioBase, edadJubilacion. • Los identificadores de constantes (datos que no van a cambiar durante la ejecución del programa) se deben escribir con todos los caracteres con mayúsculas; si el identificador está compuesto por varias palabras es aconsejable separarlas con el guion bajo ( _ ). Por ejemplo: PI, PRIMER_VALOR, EDAD_MINIMA. • Los identificadores de métodos tendrán la primera letra en minúscula, y la primera letra de las siguientes palabras en mayúscula: x, distanciaAlOrigen, etc. • Los identificadores de clases e interfaces se deben escribir con el primer carácter de cada palabra en mayúsculas y el resto en minúsculas: Punto2D, Comparable, etc. Palabras reservadas de Java Una palabra reservada es una palabra que tiene un significado especial para el compilador de un lenguaje, y, por lo tanto, no puede ser utilizada como identificador. Algunos ejemplos de palabras reservadas son main, int, return, if o for. Fundamentos de programación: JAVA Miguel Toro Bonilla Palabras reservadas de Java
Elementos básicos del lenguaje 25 Literales Son elementos del lenguaje que permiten representar valores constantes de los distintos tipos del lenguaje. Por ejemplo, 23 es un literal de tipo Integer, 23L es un literal de tipo Long, 3.14 es un literal de tipo Double, “Guadalquivir” es un literal de tipo String y true y false son los dos literales que corresponden al tipo Boolean. Para los objetos existe un literal null que representa un objeto sin valor. Comentarios Los comentarios son un tipo especial de separadores que sirven para explicar o aclarar algunas sentencias del código, por parte del programador, y ayudar a su prueba y mantenimiento. De esta forma, se intenta que el código pueda ser entendido por una persona diferente o por el propio programador algún tiempo después. Los comentarios son ignorados por el compilador. En Java existen comentarios de línea, que se marcan con //, y bloques de comentarios, que comienzan con /* y terminan con */. // Este es un comentario de una línea /* Este es un bloque de comentario que ocupa varias líneas */ Fundamentos de programación: JAVA Miguel Toro Bonilla Literales Comentarios
Expresiones y operadores 32 disponen de los métodos intValue(), longValue(), floatValue(), doubleValue() para convertir a los tipos respectivos. Precedencia y asociatividad de los operadores El resultado de una expresión depende del orden en que se ejecutan las operaciones. Por ejemplo, si queremos calcular el valor de la expresión 3 + 4 * 2, podemos tener distintos resultados dependiendo de qué operación se ejecuta primero. Así, si se realiza primero la suma (3 + 4) y después el producto (7 * 2), el resultado es 14; mientras que si se realiza primero el producto (4 * 2) y luego la suma (3 + 8), el resultado es 11. Para saber en qué orden se realizan las operaciones es necesario definir unas reglas de precedencia y asociatividad. La tabla siguiente resume las reglas de precedencia y asociatividad de los principales operadores en el lenguaje Java. Operador Asociatividad . [] () + - ! ++ -- (tipo) new derecha a izquierda * / % izquierda a derecha + – izquierda a derecha < <= > >= izquierda a derecha == != izquierda a derecha && izquierda a derecha || izquierda a derecha ? : = += –= *= /= %= derecha a izquierda Fundamentos de programación: JAVA Miguel Toro Bonilla Precedencia y asociatividad de los operadores
Expresiones y operadores 33 Los operadores que están en la misma línea tienen la misma precedencia, y las filas están en orden de precedencia decreciente. También se debe tener en cuenta que el uso de paréntesis modifica el orden de aplicación de los operadores. Si no hay paréntesis el orden de aplicación de los operadores viene definido por la precedencia y la asociatividad de estos definida arriba. Fundamentos de programación: JAVA Miguel Toro Bonilla
34 El tipo String, tipos para el manejo de fechas Tipo String Como hemos visto antes, el tipo String representa una secuencia o cadena de caracteres. El tamaño de un String es inmutable y, por lo tanto, no cambia una vez creado el objeto. Se representa por el método length. Cada carácter de la cadena ocupa una posición, comenzando por la posición 0 y terminando por la posición length()-1. Para acceder al carácter que ocupa una posición dada se utiliza el método charAt(int i). Por ejemplo; String nombre = "Amaia"; Integer lon = nombre.length() // El valor de lon será 5 Character inicial = nombre.charAt(0) // El valor de inicial //será 'A' Character ultima = nombre.charAt(lon - 1) // El valor de //ultima será 'a' No es posible modificar un carácter de la cadena una vez que se ha creado. Tampoco se pueden añadir o eliminar caracteres. El tipo String es, pues, inmutable. El tipo String ofrece otros métodos para, entre otras cosas, decidir si la cadena contiene una secuencia dada de caracteres, buscar la primera posición de un carácter u obtener la subcadena dada por dos posiciones. Los detalles de estos métodos pueden verse en la documentación de la clase String en la API de Java. Fundamentos de programación: JAVA Miguel Toro Bonilla El tipo String, tipos para el manejo de fechas Tipo String
El tipo String, tipos para el manejo de fechas 35 El tipo String es muy importante porque todos los datos que leemos de un fichero o de la consola son de tipos String. Tenemos que comprender bien como transformamos cadenas de texto a un tipo dado y viceversa. A la operación de transformar una cadena de texto en un tipo lo llamamos parse. Los tipos que nos ofrece Java ya incorporar un método de factoría para hacer este cometido. Los tipos que diseñemos nuevos es conveniente dotarlos de un método de factoría que lleve a cabo ese cometido. Integer n1 = Integer.parseInt("-345"); Long n2 = Long.parseLong(("-345"); Double r = Double.parseDouble("-34.56"); Punto2D p = Punto2D.parse(3.7,-6.7); El proceso inverso de parse en la transformación de un objeto de un tipo en una cadena de caracteres. Esto se consigue con el método toString(). Cada tipo tiene este método. También es interesante comprender como conseguir una cadena de caracteres a partir de uno o varios objetos combinados con un texto. Esto se consigue con el método format del tipo String. Este método tiene la cabecera: String String.format(String format, Object... args) El parámetro args es una secuencia de parámetros de cualquier tipo. El parámetro format es una cadena de texto que incluye especificadores de formato. Esto especificadores tienen la estructura: %[argument_index$][width][.precision]conversion Donde • argument_index (opcional) es la posición del parámetro en la lista de parámetros comenzando en 1. Si falta se va aplicando el especificador a cada parámetro consecutivamente • El parámetro width (opcional) es la longitude mínima de la cadena de salida • El parámetro precision (opcional) es un número entero que se usa normalmente para indicar el número de decimales de un número real. Fundamentos de programación: JAVA Miguel Toro Bonilla
El tipo String, tipos para el manejo de fechas 36 • El paramero conversion indica el tipo del parámetro que va a ser formateado. Los más usuales son: c (carácter), d (entero), f (número real), s (cadena de caracteres), % (produce % en la salida). Hay especificadores para fechas que veremos más adelante. Ejemplos String.format("r = %1$5.2f, porcentaje = %2$d%%",r,30L); String.format("(%3d,%30s,%2d,%2d,%2d,%s)",numero,name,slots,em pty_slots,f,c); Tipos para el manejo de fechas y horas Java dispone de un conjunto de tipos para trabajar con fechas, horas, instantes temporales y duraciones, incluidos en el paquete java.time. Algunos de estos tipos son los siguientes: • LocalDateTime, tipo inmutable para representar fechas y horas. Por ejemplo, 03-12-2015 10:34 • LocalDate, tipo inmutable para representar fechas (sin zona horaria), es decir, solo con día, mes y año. Por ejemplo, 03-12-2015 • LocalTime, tipo inmutable para representar horas sin fecha (ni zona horaria), solo con hora, minutos, segundos y nanosegundos (si son necesarios). Por ejemplo, 10:10:12.99. • Duration, tipo inmutable que representa una cantidad temporal que se mueve en el rango de nanosegundos, segundos, minutos, horas o días. • Period, tipo inmutable que representa una cantidad temporal que se mueve en el rango de años, meses o días. Estos tipos tienen métodos de factoría adecuados para construir objetos. Veamos algunas operaciones habituales con estos tipos. Comencemos por la creación de objetos de tipo fecha y hora: Fundamentos de programación: JAVA Miguel Toro Bonilla Tipos para el manejo de fechas y horas
El tipo String, tipos para el manejo de fechas 37 LocalDate fecha1 = LocalDate.of(2014, Month.MAY, 23); LocalDate fecha2 = LocalDate.of(2014, 5, 23); LocalTime hora1 = LocalTime.of(11, 00); LocalTime hora2 = LocalTime.of(10, 59, 59); Además del método of, se pueden crear objetos con la fecha y hora del sistema en el momento en que se invoca al método. Por ejemplo LocalDate fecha3 = LocalDate.now(); LocalTime hora3 = LocalTime.now(); Dada una fecha o una hora, podemos acceder a una parte de ella, como por ejemplo el día de una fecha o los minutos de una hora: LocalDate hoy = LocalDateTime.now(); Integer dia = hoy.getDayOfMonth(); DayOfWeek diaSemana = hoy.getDayOfWeek(); LocalDate ahora = LocalDateTime.now(); Integer minutos = ahora.getMinute(); También podemos sumar o restar un periodo de tiempo a una fecha u hora: LocalDate f1 = LocalDate.of(2008, Month.FEBRUARY, 29); LocalDate f2 = f1.plusYears(1); System.out.println("Un año después..." + f2); Otra operación habitual es obtener el tiempo transcurrido entre dos fechas: LocalDate fechaNacimiento = LocalDate.of(2013, 12, 3); LocalDate hoy = LocalDate.now(); Period p = fechaNacimiento.until(hoy); Period p = Period.between (fechaNacimiento, hoy); // También así System.out.println ("El periodo entre las dos fechas es " + p); System.out.println ("Los años del periodo son " + p.getYears()); System.out.println ("Los meses del periodo son " + p.getMonths()); System.out.println ("Los días de periodo son " + p.getDays()); De manera similar para las horas, pero usando el tipo Duration: Fundamentos de programación: JAVA Miguel Toro Bonilla
El tipo String, tipos para el manejo de fechas 38 LocalTime hora1 = LocalTime.of(15, 30); LocalTime hora2 = LocalTime.of(15, 45); Duration d = Duration.between(hora1, hora2); System.out.println("Duración - " + d); System.out.println ("Los segundos de la duración son " + d.getSeconds()); Para comparar dos fechas u horas, utilizamos los métodos isBefore, isAfter e isEqual: LocalDate f1 = LocalDate.of (2016, Month.SEPTEMBER, 19); LocalDate f2 = LocalDate.of (2017, Month.JANUARY, 13); System.out.println("¿Es f1 anterior a f2? " + f1.isBefore(f2)); System.out.println("¿Es f1 posterior a f2? " + f1.isAfter(f2)); System.out.println("¿Es f1 igual a f2? " + f1.isEqual(f2)); Finalmente, cuando mostramos fechas y horas en la pantalla, queremos que aparezcan en un formato concreto. Los siguientes ejemplos muestran varias formas de hacerlo: LocalDate fecha = LocalDate.of(2016, 12,3); String fechaFormateada = fecha.format(DateTimeFormatter.ofPattern("dd-MM-yyyy")); System.out.println("Fecha: " + fechaFormateada); LocalTime hora = LocalTime.now(); String horaFormateada = hora.format(DateTimeFormatter.ofPattern("HH:mm:ss:n")); System.out.println("Hora: " + horaFormateada); La salida que se produce al ejecutar el código anterior es la siguiente: Fecha: 03-12-2016 Hora: 19:00:19:56000000 Fundamentos de programación: JAVA Miguel Toro Bonilla
39 Sentencias de control selectivas ay dos tipos de sentencias de control: las bifurcaciones y los bucles. Las bifurcaciones permiten ejecutar un bloque de sentencias u otro, pero no ambos a la vez. En este tipo de sentencias de control se encuadran las sentencias if y switch. Sentencia if-else La sentencia if evalúa una expresión lógica (o condición) y, según sea cierta o falsa, ejecuta un bloque de sentencias u otro. if (n % 2 == 0) { System.out.println("El número es par "); } else { System.out.println("El número es impar"); } Las sentencias if-else pueden encadenarse haciendo que se evalúe un conjunto de condiciones y se ejecute una sola de varias opciones. H Fundamentos de programación: JAVA Miguel Toro Bonilla Sentencias de control selectivas Sentencia if-else
Sentencias de control selectivas 40 Float impuesto = 0.0; if (salario >= 5000.0) { impuesto = 20.0; } else if (salario < 5000.0 && salario >= 2500.0) { impuesto = 15.0; } else if (salario < 2500.0 && salario >= 1500.0) { impuesto = 10.0; } else if (salario > 800.0) { impuesto = 5.0; } Sentencia switch Normalmente switch se utiliza cuando se requiere comparar una variable con una serie de valores diferentes. Hay dos versiones de switch: como sentencia o como expresión. En ambos casos se indican los posibles valores que puede tomar la variable y las sentencias que se tienen que ejecutar si la variable coincide con alguno de dichos valores o las expresiones que calculan el valor resultante. Es una sentencia muy indicada para comparar una variable de un tipo enumerado con cada uno de sus posibles valores. La expresión tiene que ser de uno de los siguientes tipos: Character, Integer, String y los tipos enumerados (enum). Hay dos formas de switch: como elemento de una expresión: Character s = switch(r) { case "Lunes" -> r.charAt(0); case "Martes" -> r.charAt(1); default -> throw new IllegalArgumentException("Unexpected value: " + r); }; Y como sentencia: Fundamentos de programación: JAVA Miguel Toro Bonilla Sentencia switch
Sentencias de control selectivas 41 Character s; switch(r) { case "Lunes": s = r.charAt(0); break; case "Martes": s = r.charAt(1); break; default -> throw new IllegalArgumentException("Unexpected value: " + r); } Existe también posibilidad de ejecutar el mismo bloque de sentencias para varios valores del resultado de expresión. Se haría de la siguiente forma: switch (dia) { case 1: case 2: case 3: case 4: case 5: case 10: case 12: System.out.println("Día laborable");break; case 6: case 7: System.out.println("Fin de semana");break; } Fundamentos de programación: JAVA Miguel Toro Bonilla
48 Sentencias de control iterativas os bucles son sentencias de control que ejecutan un bloque de sentencias un número determinado de veces. En Java los bucles se implementan con las sentencias for y while. Sentencia while La sentencia while ejecuta el bloque de sentencias mientras la condición evaluada sea cierta. Su sintaxis es la que se muestra a continuación. Por ejemplo, para sumar los números comprendidos entre 1 y n, haríamos lo siguiente: Integer suma = 0; int i = 1; while (i <= n) { suma = suma + i; i++; } Sentencia for clásico La sentencia for tiene la siguiente sintaxis, donde inicialización y actualización son sentencias y condición es una expresión lógica. L Fundamentos de programación: JAVA Miguel Toro Bonilla Sentencias de control iterativas Sentencia while Sentencia for clásico
Sentencias de control iterativas 49 Sintaxis: for (inicialización; condición; actualización) { sentencia-1; sentencia-2; ... sentencia-n; } ¿Cómo funciona el for? Cuando el control de un programa llega a una sentencia for, lo primero que se ejecuta es la inicialización. A continuación, se evalúa la condición, si es cierta (valor distinto de cero), se ejecuta el bloque de sentencias de 1 a n. Una vez terminado el bloque, se ejecuta la sentencia de actualización antes de volver a evaluar la condición, si ésta fuera cierta de nuevo se ejecutaría el bloque y la actualización, y así repetidas veces hasta que la condición fuera falsa y abandonáramos el bucle. El bucle while anterior, escrito con una sentencia for, sería así: Integer suma = 0; for (int i = 0; i <= n; i++) { suma = suma + i; } Sentencia for extendido Existe una variante del for que se utiliza para recorrer iterables. Los agregados de datos ofrecen vistas iterables para recorrer sus elementos. Las listas y los conjuntos son iterables. Supongamos por ejemplo que queremos calcular el valor medio de las temperaturas de una lista temperaturas de tipo Double. Lo podríamos hacer mediante un for clásico: Double suma = 0.0; for (int i = 0; i < temperaturas.size(); i++) { suma = suma + temperaturas.get(i); } Double temperaturaMedia = suma / temperaturas.size(); Existe una forma alternativa, usando un for extendido, que resulta más simple. Se haría de la forma siguiente: Fundamentos de programación: JAVA Miguel Toro Bonilla Sentencia for extendido
Sentencias de control iterativas 50 Double suma = 0.0; for (Double t: temperaturas) { suma = suma + t; } Double temperaturaMedia = suma / temperaturas.size(); La variable t va recorriendo las temperaturas de la lista, desde la primera hasta la última. El for se puede combinar con la lectura de ficheros de la forma: Integer s = 0; List<String> lineas = lineasFromFile("datos.txt"); for(String linea: lineas){ Integer e = Integer.parseInt(linea); s = s + e; } Sentencia break La sentencia break dentro de un bloque de sentencias que forma parte de una estructura iterativa hace que el flujo de programa salte del bloque, dando por finalizada la ejecución de la sentencia iterativa. Imagina por ejemplo que queremos saber si existe una temperatura por debajo de cero en la lista. Si al recorrer la lista encontramos una, ya sabemos que la hay, así que no es necesario seguir recorriendo el resto de las temperaturas. En ese caso usamos un break para salir del bucle: Boolean temperaturaNegativa = false; for (Double t: temperaturas) { if (t < 0) { temperaturaNegativa = true; break; } } Al terminar la ejecución de este código, la variable temperaturaNegativa tendrá valor true si la lista de números contiene al menos un valor negativo, y false si no contiene ninguno. Fundamentos de programación: JAVA Miguel Toro Bonilla Sentencia break
51 Streams partir de la versión 8, Java introduce una nueva y potente forma de realizar operaciones con agregados de datos mediante streams. Un stream es similar a un iterable pero con muchas más capacidades. Cada agregado de datos ofrecerá una o más vistas en forma de stream. Se obtienen streams a partir de una lista o conjunto mediante el método stream(). Supongamos por ejemplo la lista de temperaturas vista anteriormente. La lista es iterable pero también ofrece una vista en forma de stream. Si queremos conocer el número de ellas que están por debajo de cero, podríamos hacerlo mediante el siguiente bucle: long bajoCero = 0; for (Double t: temperaturas) { if (t < 0) { bajoCero++; } } System.out.println(String.format( "Hay %d temperaturas bajo cero", bajocero)); Los streams nos ofrecen una alternativa a este bucle. Solo tenemos que obtener un stream a partir de la lista y aplicar las operaciones propias de los streams. El mismo tratamiento anterior se haría de esta otra forma: A Fundamentos de programación: JAVA Miguel Toro Bonilla Streams
Streams 52 long bajoCero = temperaturas.stream() .filter(x -> x < 0) .count(); System.out.println(String.format("Hay %d temperaturas bajo cero", bajocero)); Veamos un segundo ejemplo. En este caso queremos saber si existe alguna temperatura por encima de los 40 grados. Sería así: boolean algunaPorEncima = temperaturas.stream() .anyMatch(x -> x > 40); if (algunaPorEncima) { System.out.println("Hay al menos una temperatura mayor de 40º"); } El paso de usar bucles a usar streams supone el paso de un paradigma imperativo a un paradigma funcional, en el cual indicamos qué es lo que queremos hacer, y no cómo hacerlo; de eso se encarga ya el propio lenguaje, simplificando así nuestra tarea. Los iterables sirven para hacer tratamientos secuenciales sobre agregados de forma imperativa. Los streams son adecuados para hacer tratamientos secuenciales o paralelos sobre los agregados de manera funcional. Streams e iterables son tipos diferentes pero pueden ser convertidos uno en otro. Un iterable puede ser usado en un for extendido pero un stream no porque tiene sus propios métodos. Un stream puede ser convertido en iterable con el método iterator(). Fundamentos de programación: JAVA Miguel Toro Bonilla
53 Tipos genéricos, registros, clases e interfaces a hemos visto previamente que para diseñar tipos nuevos partimos de un diseño de tipo. Posteriormente implementamos el diseño mediante un record. Un registro (record) es un tipo especial de clase. Ahora veremos la relación entre clase y registro. Un tipo puede ser genérico. Eso quiere decir que tiene uno o varios parámetros de tipo. Esos parámetros de tipo serán posteriormente sustituidos (instanciados) por tipos concretos. Veamos un ejemplo de diseño de tipo genérico y su implementación mediante un registro. Par<A,B> Propiedades a: A, básica b: B, básica copy(): Par<A,B>, derivada Representación (a,b) Orden Natural No tiene Factoría of(A a, B b): Par<A,B> La implementación como un registro es: Y Fundamentos de programación: JAVA Miguel Toro Bonilla Tipos genéricos, registros, clases e interfaces
Tipos genéricos, registros, clases e interfaces 54 public record Par<A, B> (A a, B b) { public static <A,B> Par<A, B> of(A a, B b){ return new Par<>(a,b); } public Par<A, B> copy(){ return new Par<>(this.a,this.b); } @Override public String toString() { return String.format("(%s,%s)",this.a,this.b); } } Tal como vemos el nombre del tipo lleva unos parámetros de tipo A, B delimitados por <>. Junto con los tipos genéricos también tenemos métodos genéricos. Esto métodos tienen también parámetros de tipo que se declaran en la cabecera como en el caso del método de factoría of. Como hemos dicho los registros son una forma particular de clase. Los registros están pensados para simplificar el código de una clase. Sirven para diseñar tipos inmutables y a partir de un estado hacen implícito (sus atributos) generan un constructor, los métodos que definen la igualdad del tipo (equals y hashCode), métodos para acceder a las propiedades básicas y su representación (el método toString). Las clases permiten más flexibilidad explicitando el conjunto de atributos (estado) que podemos hacer públicos o privados. Para cada clase debemos diseñar explícitamente uno o varios constructores y a partir de la igualdad definida para el tipo los métodos equals y hashCode. Así mismo debemos implementar el método toString. Las clases, por lo tanto, son las unidades de la POO que permiten definir los detalles del estado interno de un objeto (mediante los atributos), calcular las propiedades de los objetos a partir de los atributos e implementar las funcionalidades ofrecidas por los objetos (a través de los métodos) Para escribir una clase en Java tenemos que ajustarnos al siguiente patrón: Fundamentos de programación: JAVA Miguel Toro Bonilla
Tipos genéricos, registros, clases e interfaces 55 [Modificadores] class NombreClase [extends ...] [implements ...] { [atributos] [métodos] } El patrón lo tenemos que completar con el nombre de la clase, y, de manera opcional, una serie de modificadores, una cláusula extends opcional, una cláusula implements también opcional y una serie de atributos y métodos. Ambas cláusulas las estudiaremos más abajo. En nuestra metodología para ir detallando la clase nos guiamos por el diseño del tipo. Atributos Los atributos sirven para establecer los detalles del estado interno de los objetos. Son un conjunto de variables, cada una de un tipo, y con determinadas restricciones sobre su visibilidad exterior. En el registro los atributos son implícitos. Como una primera guía dentro de nuestra metodología, la clase tendrá un atributo por cada propiedad básica y será del mismo tipo que esta propiedad. Aunque más adelante aprenderemos otras posibilidades, restringiremos el acceso a los atributos para que sólo sean visibles desde dentro de la clase. Esto implica que a la hora de declarar los atributos tenemos que anteponer la palabra reservada private a la declaración de este. Las otras posibilidades son public o protected. Como regla general, a los atributos les damos el mismo nombre, pero comenzando en minúscula, de la propiedad que almacenan. Solo definimos atributos para guardar los valores de las propiedades que sean básicas. Las propiedades derivadas, es decir, aquellas que se pueden calcular a partir de otras, no tienen un atributo asociado en general. La declaración de un atributo tiene un ámbito, que va desde la declaración hasta el fin de la clase, incluido el cuerpo de los métodos. La sintaxis para declarar los atributos responde al siguiente patrón: Fundamentos de programación: JAVA Miguel Toro Bonilla Atributos
Tipos genéricos, registros, clases e interfaces 56 [Modificadores] tipo Identificador [ = valor inicial ]; Como puede verse, un atributo puede tener, de forma opcional, un posible valor inicial. Los modificadores incluyen la declaración de la visibilidad y opcionalmente final. Cuando este aparece el atributo es no modificable. Veamos el tipo Par anterior pero considerado que sus propiedades pueden ser modificadas, por lo tanto el tipo es mutable, implementado como una clase. Lo llamamos ParM. public class ParM<A, B> { public static <A, B> ParM<A, B> of(A a, B b) { return new ParM<A, B>(a, b); } private A a; private B b; private ParM(A a, B b) { super(); this.a = a; this.b = b; } Como vemos al implementar el tipo como una clase necesitamos atributos (que definen el estado del objeto). Usualmente los declaramos privados, un constructor que también lo declaramos privado y un método de factoría público. public A getA() { return a; } public void setA(A a) { this.a = a; } Junto a lo anterior diseñamos métodos para acceder los atributos o modificarlos. Los métodos que devuelven atributos o combinación de ellos se les llama getters. Son la implementación de las propiedades del tipo. Las propiedades básicas devolverán directamente el atributo correspondiente. Las propiedades derivadas devolverán el cálculo Fundamentos de programación: JAVA Miguel Toro Bonilla
Tipos genéricos, registros, clases e interfaces 57 correspondiente a partir de los atributos. Ambos métodos es costumbre que empiecen por get y por eso se les llama getters. Si una propiedad es modificable tiene, además, asociado un método setter. Este es un método que devuelve void y modifica uno o más atributos a partir del parámetro recibido. Por lo tanto las propiedades tienen asociado un método get y si son modificables otro set. La propiedad B, como la A anterior es modificable. public B getB() { return b; } public void setB(B b) { this.b = b; } El tipo puede tener otras propiedades derivadas como la siguiente que obtiene una copia del objeto. public ParM<A, B> getCopy(){ return new ParM<>(this.a,this.b); } Al diseñar clases debemos diseñar también el método toString que indica cómo convertir el objeto en una cadena de caracteres. @Override public String toString() { return String.format("(%s,%s)",a,b); } La implementación de tipos mediante clases obliga también a implementar los métodos equals y hashCode. Ambos métodos son necesarios si los objetos del tipo van a ser usados como claves en un Map. El método equals indica cuando dos objetos del tipo son iguales. El método hashCode devuelve un entero asociado al objeto. Una especie de identificador del objeto necesario si este quiere usarse en claves de un Map, un conjunto, etc. Fundamentos de programación: JAVA Miguel Toro Bonilla
Tipos genéricos, registros, clases e interfaces 64 correspondientes. Para ello usamos el método de factoría adecuado, eliminando previamente los posibles espacios en blanco al comienzo y al final de la cadena mediante el método trim del tipo String. Cotas sobre parámetros de tipo Lo parámetros de un tipo genérico pueden ser acotados para restringir los tipos que pueden instanciarlos. Para expresar estas cotas usamos ?, extends, super y &. Veamos algunos ejemplos; • <T extends B1 & B2 & B3>: Indica que los tipos que pueden instanciar T deben ser subtipos de B1, B2 y B3. • <T extends Comparable<T>>: T debe implementar el contrato Comparable<T>. • List<? extends F> list: Los elementos de la lista tienen que ser de un subtipo de F • List<?> list: Los elementos de la lista pueden ser de cualquier tipo • List<? extends Comparable<? super T>>: Los elementos de la lista deben ser un subtipo de un supertipo de T que implemente el contrato Comparable. Fundamentos de programación: JAVA Miguel Toro Bonilla Cotas sobre parámetros de tipo
65 Reutilización y herencia n general cuando diseñamos un tipo partimos de otros ya diseñados e implementados. Java ya nos ofrece muchos otros los tenemos que diseñar e implementar nosotros. Un programa en Java está compuesto de un conjunto de tipos. Un tipo tiene propiedades que serán de otros tipos ya definidos. Decimos que el nuevo tipo usa esos otros tipos. Herencia Pero también es usual que diseñemos tipos reutilizando otros. Hay varias formas de reutilización. Una muy conocida es la herencia. Mediante este mecanismo podemos diseñar un nuevo tipo añadiendo propiedades y posiblemente restricciones a otro tipo previo. El nuevo tipo lo llamamos subtipo y al reutilizado supertipo. Decimos que el subtipo extiende al o hereda del supertipo. Un tipo también puede ser diseñado para que cumpla un determinado contrato especificado en una interface. Esto es otro tipo de herencia. Un contrato (interface) se puede definir a partir de uno o varios contratos añadiendo más propiedades y métodos. Un caso concreto de este tipo contrato es Comparable. Una interface, como un record o una clase, también define un tipo. Un tipo que especifica un contrato. El lenguaje Java está diseñado para que todos los tipos que diseñemos extiendan un tipo concreto que se llama Object. E Fundamentos de programación: JAVA Miguel Toro Bonilla Reutilización y Herencia Herencia
Reutilización y herencia 66 La herencia se representa mediante la cláusula extends (extiende) cuando un tipo reutiliza a otro implementado mediante una clase y mediante implements (implementa) cuando un tipo está obligado a cumplir un contrato definido por una interface. La herencia (extensión o implementación) establece relaciones entre tipos. Relaciones entre subtipos y supertipos. En Java la herencia entre interfaces puede ser múltiple: un hijo puede tener uno o varios padres. La interfaz hija tiene todas las signaturas (métodos) declaradas en los padres. No se puede ocultar ninguna. La sintaxis para la interfaz con herencia responde al siguiente patrón: [Modificadores] interface NombreInterfazHija extends NombreInterfazPadre [,…] { [signaturas de métodos] } Mediante extends se indica que la interfaz que estamos combinando otras interfaces padres y, posiblemente, añadiendo algunos métodos más. Entre tipo y subtipo hay una importante propiedad. Sea una variable b de un tipo B, y otra a de otro tipo A. Si B es un subtipo de A, entonces la variable b puede ser asignada a la variable a pero no al revés. Como ejemplo veamos el tipo Fraction2 que adapta el Fraction de Apache definiendo una nueva representación y añadiendo el contrato Comparable Fundamentos de programación: JAVA Miguel Toro Bonilla
Reutilización y herencia 67 class Fraction2 extends Fraction implements Comparable<Fraction> { public static Fraction2 of(int num, int den) { return new Fraction2(num, den); } private Fraction2(int num, int den) { super(num, den); } @Override public int compareTo(Fraction other) { return super.getNumerator() *other.getDenominator() – super.getDenominator() *other.getNumerator(); } @Override public String toString() { String s = ""; s = String.format("%d",super.getNumerator()); if(super.getDenominator() != 1) s = s+ String.format("%d", super.getDenominator()); return s; } } Aquí super(…) se refiere al constructor de la clase padre y super.getNumerador() a la correspondiente propiedad de la clase padre. En Java hay restricciones a la herencia: • Un record no puede extender otro record o clase ni puede ser extendido. • Una interface puede heredar de varios interfaces • Una clase puede extender una sola clase e implementar varios interfaces. Fundamentos de programación: JAVA Miguel Toro Bonilla
Reutilización y herencia 68 Clases abstractas y finales Las clases pueden tener las etiquetas de abstract y final. Una clase abstracta se declara incluyendo abstract delante de class y significa que puede tener métodos declarados abstract. Es decir que solo indicamos su cabecera, pero no su cuerpo. public abstract class RutaA implements Ruta { … public abstract Double getTiempo(); } Las clases abstractas sirven para diseñar código común a varias clases. Estas clases reutilizan el código mediante herencia. Una clase final se declara incluyendo final delante de class y significa que no puede ser extendida. Las clases finales se diseñan así para que no puedan ser extendidas y por lo tanto a partir de ellas no se puede diseñar nueva funcionalidad mediante herencia. Los records tampoco pueden ser extendidos ni pueden extender otra clase o record. Reutilización mediante delegación La herencia (en el sentido de una clase que extiende otra) es un mecanismo de reutilización, pero impone algunas restricciones importantes en la medida que la clase hija hereda todos los métodos de la clase padre. Es decir, es un subtipo de ella. No es posible heredar solo una parte de los métodos de la clase padre. Esto en muchos casos no es adecuado por lo que el mecanismo de herencia usado para reutilizar no es útil en muchos casos. Tampoco es posible heredar de varias clases y menos heredar una parte concreta de una clase y otra parte de otra. En estos casos el mecanismo adecuado es la reutilización mediante delegación. Este mecanismo consiste esencialmente en incluir uno o varios objetos como atributos privados y delegar en ellos la implementación de algunos métodos. Fundamentos de programación: JAVA Miguel Toro Bonilla Clases abstractas y finales Reutilización mediante delegación
Reutilización y herencia 69 Veamos como ejemplo la implementación del interface Print. interface Print { void print(String s); } Diseñamos la clase Printer que implementará la interface Print y delegará en un objeto de tipo PrintStream que tiene, entre otras funcionalidades la posibilidad de imprimir una cadena de caracteres en un dispositivo. Hemos definido un método de factoría para construir un objeto Printer. Este método recibe un objeto de tipo PrintStream en el que se delegará la impresión. Ejemplos de objetos que imprementan PrintStream son System.out, System.err o un fichero abierto para escritura. class Printer implements Print { private PrintStream p; public static Printer of(PrintStream p) { return new Printer(p); } private Printer(PrintStream p) { this.p = p; } @Override public void print(String s) { p.println(s); } } Según el objeto que pasemos como parámetro a la factoría de la clase Printer podemos conseguir imprimir una cadena en la consola de salida, en la salida de errores o en un fichero adecuado. Podemos ver los ejemplos en el código siguiente. Fundamentos de programación: JAVA Miguel Toro Bonilla
Reutilización y herencia 70 void main(String[] args) { Printer p1 = Printer.of(System.out); p1.print("Hola"); Printer p2 = Printer.of(System.err); p2.print("Hola"); Printer p3 = Printer.of("ficheros/prueba.txt"); p3.print("Hola"); } Con esta técnica podemos reutilizar funcionalidades de varios tipos lo que no era posible con la herencia. Los conceptos de agregación y composición están relacionados con el concepto anterior. Composición: Un objeto que consta de otros objetos que, a su vez, no pueden existir después de que el objeto sea eliminado. Agregación: Un objeto que consta de otros objetos que pueden vivir incluso después de que el objeto sea eliminado. Veremos ejemplos en adelante. Grafo de tipos Un programa Java se compone de un conjunto de tipos entre los que existe relaciones de tipo-subtipo. Como ha hemos dicho si un tipo extiende otro o lo implementa es su subtipo. El conjunto de tipos de un programa más las relaciones de tipo-subtipo forman el grafo de tipos representamos los tipos. El grafo de tipos es útil para visualizar el conjunto de tipos ofrecidos por un objeto. El conjunto de tipos ofrecidos por un objeto está formado por todos los supertipos del tipo asociado a la clase que creó el objeto. En este conjunto siempre se añade un tipo proporcionado por Java y que es ofrecido por todos los objetos: el tipo Object. Otro uso importante del grafo de tipos es para decidir cuando una variable se puede asignar a otra. Una variable B b se podrá asignar a otra A a solo si B es un subtipo de A. Fundamentos de programación: JAVA Miguel Toro Bonilla Grafo de tipos
Reutilización y herencia 71 Junto a esta relación entre los tipos hay una más que no se suele representar en el grafo de tipos. Es la relación de uso. Un tipo A usa a otro B cuando declara algún parámetro formal, tipo de retorno, atributo o variable local del tipo B. Fundamentos de programación: JAVA Miguel Toro Bonilla
72 Igualdad, identidad y orden natural Igualdad e Identidad En Java los tipos que comienzan con mayúscula los llamamos tipos objetos. String, Integer, Par, son tipos objetos. Sin embargo int, double, long no lo son. Las instancias de tipos objeto son objetos. Los tipos no objeto tienen valores que no son objetos. Los objetos tienen identidad. Los valore no. Dos objetos son idénticos cuando el cambio de una propiedad de uno afecta a la propiedad del otro. Esto es importante sobre todo para los tipos mutables. Dos objetos son iguales cuando los valores de sus propiedades observables son iguales. Por otro lado, dos objetos son idénticos cuando al modificar una propiedad observable de uno de ellos, se produce una modificación en la del otro y viceversa. De lo anterior, se deduce que identidad implica igualdad, pero igualdad no implica identidad. Es decir, la identidad de un objeto permanece inalterada aunque cambien sus propiedades. Dos objetos no idénticos pueden tener las mismas propiedades y entonces son iguales. Dos objetos idénticos siempre tienen las mismas propiedades. Los valores de los tipos primitivos pueden tener igualdad pero no tienen identidad. El operador new crea objetos con una nueva identidad. El operador de asignación entre tipos objeto asigna la identidad del objeto de la derecha al objeto de la izquierda. Después de asignar el objeto b al a (a = b; ) ambos objetos son idénticos y, por lo tanto, si modificamos las propiedades de a quedan modificadas las de b y al revés. Entre tipos Fundamentos de programación: JAVA Miguel Toro Bonilla Igualdad, identidad y orden natural Igualdad e Identidad
Igualdad, identidad y orden natural 73 primitivos el operador de asignación asigna valores. Si m y n son de tipos primitivos, entonces la sentencia m = n; asigna el valor de n a m. Después de la asignación, m y n tienen el mismo valor, pero si modificamos el valor de m, no queda modificado el de n porque los elementos de tipos primitivos no tienen identidad, sólo tienen valor. El operador == aplicado entre tipos primitivos decide si los valores de los operandos son iguales. int i = 7; int j = 4; int k = j; boolean a = (i == j); // a es false boolean b = (k == j); // b es true El valor de a es false. Se comparan los valores de tipos primitivos y estos valores son distintos. Después de asignar j a k sus valores son iguales luego b es true. Este mismo operador aplicado entre tipos objeto decide si ambos objetos son idénticos o no. Para decidir si los objetos son iguales utilizamos el método equals, que se invoca mediante la expresión o1.equals(o2) y que devuelve un valor tipo boolean: Punto2D p1 = Punto2D.of(1.0, 2.0); Punto2D p2 = Punto2D.of(1.0, 2.0); boolean c = (p1 == p2); // c es false boolean d = p1.equals(p2); // d es true Los objetos p1 y p2 han sido creados con dos identidades distintas (cada vez que llamamos al operador new, o un método de factoría, se genera una nueva identidad) y no han sido asignados entre sí (no son idénticos) pero son iguales porque sus propiedades los son. Por ello c es false y d es true. Veamos otro ejemplo: Fundamentos de programación: JAVA Miguel Toro Bonilla
Restricciones y excepciones 80 record Circulo2D(Punto2D centro,Double radio) { public static Circulo2D of(Punto2D centro, Double radio) { Preconditions.checkArgument(radio>=0, String.format( "El radio debe ser mayor o igual a cero y es %.2f",radio)); return new Circulo2D(centro, radio); } Los métodos pueden disparar excepciones de diversos tipos y en algunos casos documentar este hecho en su cabecera mediante throws. public static void dividir() throws ArithmeticException { System.out.println(dividendo / divisor); } Gestión de excepciones En el ejemplo anterior, el programa finaliza lanzando una excepción y mostrando el correspondiente mensaje de error en la consola. Hay otras excepciones que se pueden disparar ligadas a problemas con los ficheros, con las operaciones aritméticas, etc. Podemos gestionar la excepción de forma que el programa finalice de una forma más controlada. Para ello, en lugar de lanzar la excepción, la podemos capturar, es decir, detectar que se ha producido, y tomar las acciones necesarias para que el programa finalice de la forma más adecuada posible. Para ello usamos la cláusula try/catch. Veamos cómo sería en el ejemplo anterior. try { … } catch (Exception e) { … } Dentro del bloque try se intenta a las sentencias que pueden lanzar una excepción. El bloque catch captura esta excepción antes de que se aborte Fundamentos de programación: JAVA Miguel Toro Bonilla Gestión de excepciones
Restricciones y excepciones 81 el programa ya realiza la corrección adecuada. El programa continúa su ejecución con la instrucción que sigue al bloque. La llamada a un método que incluye throws en su cabecera debe hacerse dentro de una cláusula try/catch. Fundamentos de programación: JAVA Miguel Toro Bonilla
82 Agregados de datos nteriormente presentamos el concepto de agregados de datos y vimos el manejo básico de algunos de ellos, concretamente los tipos List, Set y Map. La adecuada utilización de tipos de datos agregados predefinidos en la API de Java, como los citados, simplifica mucho el desarrollo de programas correctos en Java y tiene muchas otras ventajas: • Reduce el esfuerzo de programación puesto que proporciona estructuras de datos y algoritmos útiles. • Incrementa la velocidad y la calidad de los programas, puesto que Java ofrece implementaciones optimizadas y libres de errores. • Simplifica la interoperabilidad y la reemplazabilidad entre aplicaciones, puesto que facilita estructuras que se pueden intercambiar entre distintos componentes. • Reduce esfuerzos de aprendizaje y diseño. En este capítulo vamos a ver las colecciones, modeladas mediante la interfaz Collection y dos de las interfaces que heredan de ella: List, que modela las listas, y Set, que modela los conjuntos. También veremos el tipo Map, que modela el concepto matemático de Aplicación. La interfaz Collection La interfaz Collection, que define un tipo que podemos denominar Colección, está definida en el paquete java.util. Una colección es un tipo A Fundamentos de programación: JAVA Miguel Toro Bonilla Agregados de datos La interfaz Collection
Agregados de datos 83 muy general, que agrupa objetos (elementos) de un mismo tipo; su comportamiento específico viene determinado por sus subinterfaces, que pueden admitir elementos duplicados o no, y cuyos elementos pueden estar ordenados o no según determinado criterio. No existe una implementación específica de la interfaz Collection; sí la tienen sus subinterfaces. El tipo Collection se utiliza para declarar variables o parámetros donde se quiere la máxima generalidad posible, esto es, que puedan ser utilizados tanto por listas como por conjuntos (Collection tiene otros subtipos que se tratarán más adelante). La interfaz Collection es genérica, por lo que hablaremos de Collection<E>. Hereda de Iterable<E>, lo que implica que las colecciones, así como las listas y los conjuntos, subtipos suyos, son iterables y se puede utilizar con ellas el for extendido. La siguiente figura representa la jerarquía de interfaces de las colecciones. Las operaciones del tipo Collection se especifican a continuación. En general, el valor devuelto por los métodos add, addAll, remove, removeAll y retainAll será true si la colección queda modificada por la aplicación del método y false en caso contrario. boolean add(E e) Añade un elemento a la colección, devuelve false si no se añade. Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 84 boolean addAll(Collection<? extends E> c) Añade todos los elementos de c a la colección que invoca. Es el operador unión. Devuelve true si la colección original se modifica. void clear() Borra todos los elementos de la colección. boolean contains(Object o) Devuelve true si o está en la colección invocante. boolean containsAll(Collection<?> c) Devuelve true si la colección que invoca contiene todos los elementos de c. boolean isEmpty() Devuelve true si la colección no tiene elementos. boolean remove(Object o) Borra el objeto o de la colección que invoca; si no estuviera se devuelve false. boolean removeAll(Collection<?> c) Borra todos los objetos de la colección que invoca que estén en c. Devuelve true si la colección original se modifica. boolean retainAll(Collection<?> c) En la colección que invoca sólo se quedarán aquellos objetos que están en c. Por tanto, es la intersección entre ambas colecciones. Devuelve true si la colección original se modifica. int size() Devuelve el número de elementos. El tipo List Conceptualmente, las listas representan colecciones de elementos en los que importa cuál es el primero, el segundo, etc. Cada elemento está referenciado mediante un índice; el índice del primer elemento es el 0. Las listas pueden contener elementos duplicados. Fundamentos de programación: JAVA Miguel Toro Bonilla El tipo List
Agregados de datos 85 Desde el punto de vista de la API de Java, las listas son un subtipo de Collection: la interfaz List hereda de la interfaz Collection. Inicializar listas con varios elementos puede hacerse de las formas: List<String> ls0 = Arrays.asList("A","B","C","B","B"); List<String> ls1 = List.of("A","B","C","B","B"); La lista ls0 es mutable y la ls1 inmutable. Por otra parte las listas tienen todas las operaciones vistas para las colecciones, aunque debemos prestar atención a la semántica de algunas operaciones: La operación addAll añade los elementos de la lista que se pasa al final de la lista sobre la que se invoca. Si en la lista sobre la que se invoca hay elementos duplicados, la operación removeAll elimina de esta todas las instancias de los elementos que aparecen en la lista que se pasa. De manera análoga se comporta retainAll: si en la lista sobre la que se invoca un elemento aparece n veces, y este aparece en la lista que se pasa (independientemente del número de veces que aparezca), en la lista resultado permanecerán las n apariciones del elemento. Veamos esto con un ejemplo. Si se ejecuta el siguiente código, Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 86 List<String> l1 = new ArrayList<String>(); List<String> l2 = new ArrayList<String>(); l1.add("A"); l1.add("B"); l1.add("C"); l2.add("B"); l2.add("B"); l1.removeAll(l2); System.out.println("l1 después de l1.removeAll(l2): " + l1); l1.clear(); l2.clear(); l1.add("A"); l1.add("B"); l1.add("C"); l2.add("B"); l2.add("B"); l2.removeAll(l1); System.out.println("l2 después de l2.removeAll(l1): " + l2); l1.clear(); l2.clear(); l1.add("A"); l1.add("B"); l1.add("C"); l2.add("B"); l2.add("B"); l2.retainAll(l1); System.out.println("l2 después de l2.retainAll(l1): " + l2); Se obtiene como salida lo siguiente: l1 después de l1.removeAll(l2): [A, C] l2 después de l2.removeAll(l1): [] l2 después de l2.retainAll(l1): [B, B] La interfaz List aporta varias operaciones específicas, que no aparecen en Collection; en las operaciones que tienen índice, si no se cumple la restricción sobre este se eleva la excepción IndexOutOfBoundsException. void add(int index, E element) Inserta el elemento especificado en la posición especificada. El que estaba previamente en la posición index pasará a la posición index + 1 , el que estaba en la posición index + 1 pasará a la Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 87 posición index + 2, etc. Los valores lícitos para index son 0 ≤ index ≤ size(). boolean addAll(int index, Collection<? extends E> c) Inserta todos los elementos de c en la posición especificada, desplazando el que estaba previamente en la posición index a la posición index + c.size(), etc. Los valores lícitos para index son 0 ≤ index ≤ size(). E get(int index) Devuelve el elemento de la lista en la posición especificada. Los valores lícitos para index son 0 ≤ index < size(). int indexOf(Object o) Devuelve el índice donde se encuentra por primera vez el elemento o (si no está devuelve -1). int lastIndexOf(Object o) Devuelve el índice donde se encuentra por última vez el elemento o (si no estuviera devuelve -1). E remove(int index) Borra el elemento de la posición especificada. Los valores lícitos para index son 0 ≤ index < size(). E set(int index, E element) Reemplaza el elemento de la posición indicada por el que se da como argumento. Los valores lícitos para index son 0 ≤ index < size(). List<E> subList(int fromIndex, int toIndex) Devuelve una vista de la porción de la lista entre fromIndex, inclusive, and toIndex , sin incluir. Los valores lícitos de fromIndex y toIndex son 0 ≤ fromIndex ≤ toIndex ≤ size(). La operación subList devuelve una vista de la lista original. Esto quiere decir que las operaciones que se realicen sobre la sublista se verán reflejadas en la lista original y viceversa. Hay que prestar atención al hecho de que si se produce un cambio estructural que afecte al tamaño de Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 88 la lista original, pueden ocurrir comportamientos extraños, en particular la elevación de una excepción cuando se utiliza la sublista. Veamos el comportamiento de subList con un ejemplo. Supongamos que se ejecuta el código: List<String> ls = new LinkedList<String>(); ls.add("A"); ls.add("B"); ls.add("C"); ls.add("D"); ls.add("E"); List<String> subLs = ls.subList(1, 4); System.out.println("Sublista: " + subLs); ls.set(2, "F"); System.out.println( "Sublista después de modificar el elemento 2 de la lista: "+ subLs); subLs.remove(1); System.out.println("Sublista después de eliminar el elemento 1: "+ subLs); System.out.println( "Lista original después de modificar la sublista: " + ls); subLs.add("X"); subLs.add("Y"); // añade "X" e "Y" al final de la sublista System.out.println("Sublista después de añadirle X e Y: " + subLs); System.out.println("Lista después de la modificación de la sublista: "+ ls); ls.remove(0); System.out.println("Lista después de eliminar el primer elemento: "+ ls); El resultado que se obtiene como salida será el siguiente: Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 89 Sublista: [B, C, D] Sublista después de modificar el elemento 2 de la lista: [B, F, D] Sublista después de eliminar el elemento 1: [B, D] Lista original después de modificar la sublista: [A, B, D, E] Sublista después de añadirle X e Y: [B, D, X, Y] Lista después de la modificación de la sublista: [A, B, D, X, Y, E] Lista después de eliminar el primer elemento: [B, D, X, Y, E] En este otro ejemplo, si se ejecuta el código: List<String> ls = new LinkedList<String>(); ls.add("A"); ls.add("B"); ls.add("C"); ls.add("D"); ls.add("E"); List<String> subLs = ls.subList(1, 4); System.out.println("Sublista: " + subLs); ls.remove(2); System.out.println("Sublista después de modificar la lista: "+ subLs); Se obtiene como salida: Sublista: [B, C, D] Exception in thread "main" java.util.ConcurrentModificationException Las implementaciones de las listas son ArrayList y LinkedList1. La elección de una u otra implementación dependerá del tipo de operaciones que realicemos sobre ellas. Si se va a acceder preferentemente a los elementos mediante índice o a realizar búsquedas, debe usarse ArrayList. Si preferentemente se van a realizar operaciones de inserción o borrado al principio o al final debe usarse LinkedList. Las dos clases tienen dos constructores cada una: uno sin argumentos, que construye una lista vacía y otro que recibe una Collection y construye una lista con los Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 96 SortedSet<Character> ss = new TreeSet<Character>(); ss.add('X'); ss.add('C'); ss.add('F'); ss.add('P'); ss.add('R'); ss.add('Q') for (Character ch: ss) { System.out.println(ch); } Produce la siguiente salida: C F P Q R X La clase de utilidad Collections El paquete java.util contiene la clase de utilidad Collections. Esta clase está formada por métodos estáticos que permiten realizar operaciones sofisticadas sobre las colecciones, como invertir una lista, barajarla, ordenarla, buscar una sublista dentro de una lista, encontrar el máximo o el mínimo de los elementos de una colección, contar las veces en las que aparece un elemento, etc. Algunos de sus métodos (los más usados) son: static <T> boolean addAll(Collection<? super T> c, T... elements) Añade a la colección los elementos indicados en elements. static void fill(List<? super T> l, T o) Reemplaza todos los elementos de la lista l por o. Fundamentos de programación: JAVA Miguel Toro Bonilla La clase de utilidad Collections
Agregados de datos 97 static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) Devuelve el elemento máximo de la colección coll según el orden natural de sus elementos. static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll) Devuelve el elemento mínimo de la colección coll según el orden natural de sus elementos. static void reverse(List<?> list) Invierte los elementos de la lista list. static void shuffle(List<?> list) Mezcla aleatoriamente los elementos de la lista list. static <T extends Comparable<? super T>> void sort(List<T> list) Ordena la lista según el orden natural del tipo. Veamos un ejemplo de uso de la clase Collections. Si se ejecuta el código List<String> l = new LinkedList<String>(); l.add("R"); l.add("T"); l.add("B"); l.add("A"); l.add("M"); System.out.println(l); Collections.reverse(l); System.out.println(l); Collections.sort(l); System.out.println(l); Collections.fill(l, "X"); System.out.println(l); La salida será: Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 98 [R, T, B, A, M] [M, A, B, T, R] [A, B, M, R, T] [X, X, X, X, X] El tipo Map Definición El tipo de dato Map, incluido en el paquete java.util, permite modelar el concepto de aplicación: una relación entre los elementos de dos conjuntos de modo que a cada elemento del conjunto inicial le corresponde uno y solo un elemento del conjunto final. Los elementos del conjunto inicial se denominan claves (keys) y los del conjunto final valores (values). En la figura puede verse un ejemplo de Map. En este caso las claves son cadenas de caracteres, cada una de las cuales representa un valor numérico en diversas representaciones textuales, y los valores son enteros. Cada cadena tiene asociada el valor numérico correspondiente. Fundamentos de programación: JAVA Miguel Toro Bonilla El tipo Map Definición
Agregados de datos 99 Este mismo ejemplo se podría representar como una tabla con dos columnas: Clave Valor “uno” 1 “1” 1 “I” 1 “1.0” 1 “2” 2 “II” 2 “1 + 1” 2 Vemos que la tabla refleja la misma información que el diagrama anterior. Como entre las claves no puede haber elementos duplicados (una clave no puede estar asociada con más de un valor), las claves forman un conjunto (Set). Sin embargo sí que puede haber valores duplicados, por los que estos están en una Collection. Por tanto, un Map está definido por un conjunto de claves, una colección de valores y un conjunto de pares clave-valor (también llamadas entradas), que son realmente los elementos de los que está compuesto. Dentro de la jerarquía de tipos de Java, Map no extiende a Collection ni a Iterable. Por tanto, no se puede aplicar un for extendido sobre sus elementos, es decir, sobre sus pares, salvo que accedamos explícitamente a ellos. Los Map se utilizan en muchas situaciones. Por ejemplo, los listines telefónicos (clave: nombre del contacto, valor: número de teléfono), listas de clase (clave: nombre del alumno, valor: calificaciones), ventas de un producto (clave: código de producto, valor: total de euros de recaudación), índices de palabras por páginas en un libro (clave: palabra, valor: lista de números de página donde aparece), la red de un Metro (clave: nombre de la estación, valor: conjunto de líneas que pasan por esa estación, o al revés, clave: número de línea, valor: lista de estaciones de Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 100 esa línea), etc. Tiene también muchas otras aplicaciones en informática: representar diccionarios o propiedades, almacenar en memoria tablas de bases de datos, cachés, etc. Centrándonos en Java, la interfaz Map tiene dos tipos genéricos, que suelen denominarse K (de Key) y V (de Value): Map<K, V>. El Map del ejemplo anterior sería Map<String, Integer>. La clase que implementa la interfaz Map es HashMap (aunque también se puede utilizar TreeMap, que se verá más adelante). La clase HashMap obliga a definir de forma correcta el método hashCode del tipo de las claves para evitar comportamientos incorrectos, como claves repetidas. Asimismo, al igual que pasa en el tipo Set, los objetos que se introducen en un Map son “vistas” o referencias a objetos y, por tanto, si estos cambian, el Map puede dejar de ser coherente. En general, deben usarse claves que sean tipos inmutables. Un Map cuyas claves sean String y cuyos valores sean Integer se definiría e inicializaría de la siguiente forma: Map<String, Integer> m = new HashMap<String, Integer>(); Inicializar maps con varios elementos puede hacerse de las formas: Map<String,Integer> m0 = New HashSet<String>( Map.of("A",24,"B",22,"C",33,"F",55)); Map<String,Integer> m1 = Map.of("A",24,"B",22,"C",33,"F",55); El m0 es mutable y el m1 inmutable. Métodos del tipo Map void clear() Elimina todos los elementos (pares o entradas) del Map. boolean containsKey(Object key) Devuelve true si el Map contiene la clave especificada. Fundamentos de programación: JAVA Miguel Toro Bonilla Métodos del tipo Map
Agregados de datos 101 boolean containsValue(Object value) Devuelve true si una o más claves del Map tienen asociadas el valor especificado. V get(Object key) Devuelve el valor asociado con la clave especificada o null si esa clave no está asociada con ningún valor (la clave no está en el conjunto de claves). boolean isEmpty() Devuelve true si el Map no contiene ningún par. Set<K> keySet() Devuelve un Set que es una vista de las claves que contiene el Map. V put(K key, V value) Asocia el valor con la clave especificada. Devuelve el valor previamente asociado con la clave si esta ya estaba en el Map o null, en caso contrario. V remove(Object key) Elimina el par que tiene como clave el parámetro especificado. Devuelve el valor previamente asociado con la clave, o null si la clave no existía. int size() Devuelve el número de pares del Map. Collection<V> values() Devuelve una Collection que es una vista de los valores del Map. Set<Map.Entry<K,V>> entrySet() Devuelve una vista del conjunto de todos los pares del Map (el tipo Map.Entry se verá más adelante). Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 102 Los métodos del tipo Map se relacionan a continuación. La información completa sobre este tipo se puede encontrar en la API de Java. Tanto los conjuntos devueltos por keySet y entrySet como la colección devuelta por values son vistas del Map original, por lo que las modificaciones que se realicen sobre estos repercuten en los pares almacenados en el Map original (con las posibles repercusiones negativas) y viceversa. Hay que destacar que esos tres objetos son iterables. El orden en el que el iterador recorre los elementos de los conjuntos (keySet y entrySet) o la colección (values) es impredecible. La clase HashMap tiene dos constructores: el constructor sin argumentos, que construye un Map vacío, y el constructor con un argumento de tipo Map (constructor copia), que construye un Map con los mismos pares que el Map que se le pasa como argumento (equivale a crear un Map vacío y aplicar putAll). Existe una clase LinkedHashMap, que se comporta igual que HashMap excepto en que los iteradores sobre las claves, los valores o los pares, los devuelven en el orden en que se insertaron. La operación equals entre dos maps devuelve true si y solo si los conjuntos devueltos por entrySet en ambos conjuntos (sus pares) son iguales. El tipo Map.Entry Los pares de elementos (también llamados entradas) de los que está compuesto un Map<K, V> son de un tipo que viene implementado por la interfaz Map.Entry<K, V>. Esta interfaz, aparte de la operación equals que permite comparar pares para comprobar su igualdad (y el correspondiente hashCode), tiene tres operaciones: void putAll(Map<? extends K, ? extends V> m) Añade al Map todos los pares contenidos en m. Tiene el mismo efecto que hacer put de todos los elementos de m. Fundamentos de programación: JAVA Miguel Toro Bonilla El tipo Map.Entry
Agregados de datos 103 K getKey() Devuelve la clave del par. V getValue() Devuelve el valor del par. V setValue(V value) Modifica el valor del par, dándole como nuevo valor value. Devuelve el valor (segunda componente) previo del par. Por ejemplo, si tenemos una entrada (par) p que asocie “II” con 2, p.getKey() devuelve la cadena “II” y p.getValue() devuelve el entero 2; si se escribe p.setValue(3), devuelve el entero 2 y modifica el valor asociado con la clave dándole el valor 3. El toString de las entradas es de la forma clave=valor, de modo que la representación como cadena del par resultante sería II=3. Otras operaciones sobre maps Como hemos visto los objetos de tio Map<K,V> en Java son un tipo mutable. Como en el caso de listas y conjuntos algunas operaciones de modificación devuelven algún tipo de datos distinto al propio map. Veamos otro conjunto de funciones sobre maps disponibles en el repositorio que devuelven un nevo map cuando hacemos una operación de actualización. Esán disponible en la clase Map2 del repositorio. Fundamentos de programación: JAVA Miguel Toro Bonilla Otras operaciones sobre maps
Agregados de datos 104 static <V,E> String toString(Map<E,V> m) { return m.keySet().stream() .map(key->String.format( "(%s,%s)",key,m.get(key))) .collect(Collectors.joining("\n")); } static <V,E> String toString(Map<E,V> m, String sep, String prefix, String suffix) { return m.keySet().stream() .map(key->String.format( "(%s,%s)",key,m.get(key))) .collect(Collectors.joining(sep,prefix,suffix)); } static <K,V> Map<V,List<K>> invert(Map<K,V> m) { return m.keySet().stream() .collect(Collectors .toMap(x->m.get(x),x->List.of(x),List2::union)); } static <K, V> Map<K, V> of(Entry<? extends K, ? extends V>... entries) { Map<K, V> result = new HashMap<>(entries.length); for (Entry<? extends K, ? extends V> entry : entries) if (entry.getValue() != null) result.put(entry.getKey(), entry.getValue()); return result; } Fundamentos de programación: JAVA Miguel Toro Bonilla
Agregados de datos 105 static <K,V> Map<K,V> of(K key,V value){ Map<K,V> m = new HashMap<>(); m.put(key,value); return m; } static <K,V> Map<K,V> of(K key1,V value1,K key2,V value2){ Map<K,V> m = new HashMap<>(); m.put(key1,value1); m.put(key2,value2); return m; } static <K,V> Map<K,V> of(K key1,V value1,K key2,V value2,K key3,V value3){ Map<K,V> m = new HashMap<>(); m.put(key1,value1); m.put(key2,value2); m.put(key3,value3); return m; } static <K, V> Map<K, V> merge(Map<K,V> m1, Map<K,V> m2) { Map<K, V> r = new HashMap<>(m1); r.putAll(m2); return r; } static <K, V> Entry<K,V> entry(K key, V value) { return new AbstractMap.SimpleEntry<>(key,value); } El tipo SortedMap El tipo SortedMap es un subtipo de Map en el que el conjunto de las claves está ordenado. La clase que implementa SortedMap es TreeMap. Es necesario que el tipo de las claves tenga un orden natural (es decir, que implemente Comparable), o bien que se indique el orden mediante un comparador. Fundamentos de programación: JAVA Miguel Toro Bonilla El tipo SortedMap
112 Interfaces funcionales y lambda expresiones ara trabajar con streams se unas un estilo de programación funcional y fluente. En este estilo es muy usual pasar funciones como parámetros. Estas funciones, como otros parámetros, deben tener un tipo. En Java se usan interfaces funcionales para especificar el tipo de una función. Una interface funcional es una interface son un solo método individual aunque puede tener varios métodos estáticos o default. La signatura de ese método individual especifica el tipo de una función: es decir un tipo funcional. Al igual que los tipos numéricos se inicializan con valores los tipos funcionales se inicializan con lambda expresiones. Una lambda expresión es la forma de codificar un método anónimo. Cada lambda expresión tiene un tipo funcional y puede ser asignada a una variable declarada de una interface funcional compatible. Una expresión lambda se escribe de la forma de una de las formas: x->x.exp; (x,y)-> {exp1; exp2; return exp3;}; Como se ve tienen uno o varios parámetros (o ninguno) y un cuerpo donde calculan un resultado a partir de los parámetros. Parámetros y resultado están separados por ->. Iremos viendo ejemplos en lo que sigue. Es importante saber encontrar en tipo funcional de una lambda expresión. Sea la interface funcional: P Fundamentos de programación: JAVA Miguel Toro Bonilla Interfaces funcionales y lambda expresiones
Interfaces funcionales y lambda expresiones 113 public interface MiInterface { public Integer calcula(Integer a1, Integer a2); } Con el podemos declarar e inicializar la variable: MiInterface v = (a, b) -> 2*a+b; Tanto la interface funcional como la lambda expresión tienen tipo funcional 𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖×𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖→𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖 por lo que la asignación es posible. Podemos hacer el cálculo: Ingteger r = v.calcula(23,45); Java introduce el operador llamada a método (::) para simplificar la escritura de lambda expresiones. Siendo 𝑇𝑇𝑇𝑇 un tipo, me un método estático del mismo, mi un método individual y obj un objeto tenemos la equivalencia de este operador con en expresiones lambda: T::me ≡ (x,y)->T.me(x,y) obj::mi ≡ (x,y)->x.mi(y) Los interfaces funcionales tienen nombres que vamos a ir aprendiendo. Veamos las interfaces funcionales más conocidas. La interfaz funcional Predicate Es una interface que define el tipo funcional 𝐸𝐸𝐸𝐸→𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝑖𝑖𝑖𝑖 y es de la forma: public interface Predicate<T> { boolean test(T o); default Predicate<T> and(Predicate<? super T> p); default Predicate<T> negate(); default Predicate<T> or(Predicate<? super T> p); } Vemos que incluye algunos métodos estáticos para combinar predicados como and, or y not. Ejemplo: Fundamentos de programación: JAVA Miguel Toro Bonilla La interfaz funcional Predicate
Interfaces funcionales y lambda expresiones 114 Predicate<Fraction> p = f->f.getNumerador() > f.getDenominador(); La interfaz Function y Bifunction El tipo funcional definido por Function<E,R> es 𝐸𝐸𝐸𝐸→𝑅𝑅𝑅𝑅 y su interface: public interface Function<E, R> { R apply(E o); default <V> Function<E, V> andThen( Function<? super R, ? super V> after); default <V> Function<V, R> compose( Function<? super V, ? super E> before); } Ejemplo: Function<String,Punto2D> f = Punto2D::parse; Los métodos compose y andThen de la interfaz Function permiten componer dos funciones una tras otra. Así, f1.andThen(f2) construye una nueva función que aplica sucesivamente f1 y luego f2 a los resultados obtenidos. Por su parte, f1.compose(f2) construye una nueva función que aplica sucesivamente f2 y luego f1 a los resultados obtenidos. El interface funcional Bifunction<E,U,R> tiene el tipo funcional 𝐸𝐸𝐸𝐸×𝑈𝑈𝑈𝑈→𝑅𝑅𝑅𝑅 public interface BiFunction<E, U, R> { R apply(E e, U u); } Las interfaces Unaryoperator y BinaryOperator UnaryOperator<E> y BinaryOperator<E> son casos particulares de Function y Bifunction. Tienen los tipos funcionales 𝐸𝐸𝐸𝐸→𝐸𝐸𝐸𝐸 y 𝐸𝐸𝐸𝐸×𝐸𝐸𝐸𝐸→𝐸𝐸𝐸𝐸 respectivamente y los interfaces funcionales Fundamentos de programación: JAVA Miguel Toro Bonilla La interfaz Function y Bifunction Las interfaces Unaryoperator y BinaryOperator
Interfaces funcionales y lambda expresiones 115 public interface UnaryOperator<E> { E apply(E e); static <T> UnaryOperator<T> identity(); } public interface BinaryOperator<E> { E apply(E e1, E e2); static <E> BinaryOperator<T> maxBy( Comparator<? super T> comparator); static <E> BinaryOperator<T> minBy( Comparator<? super T> comparator); } Ejemplos: UnaryOperator<Integer> uo = e->e+1; BinaryOperator<Integer> bo = (a,b)-> BinaryOperator .minBy(Comparator.naturalOrder()); La interfaz Consumer y Biconsumer La interfaz funcional Consumer tiene asociado el tipo funcional 𝐸𝐸𝐸𝐸→𝑣𝑣𝑣𝑣𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝑣𝑣𝑣𝑣 y BiConsumer 𝐸𝐸𝐸𝐸×𝑈𝑈𝑈𝑈→𝑣𝑣𝑣𝑣𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝐵𝑣𝑣𝑣𝑣. Su objetivo es realizar una acción sobre el objeto, o par de objetos, sin devolver ningún valor. public interface Consumer<E> { void accept(E e); } public interface BiConsumer<E,U> { void accept(E e, U u); } Ejemplo: Consumer<Punto2D> c = c->System.out.println(c); Consumer<Punto2D> c = System.out::println; La interface Supplier La interfaz funcional Supplier tiene asociado el tipo funcional () →𝐸𝐸𝐸𝐸. Es el tipo funcional adecuado para representar los métodos de factoría de un tipo. Fundamentos de programación: JAVA Miguel Toro Bonilla La interfaz Consumer y Biconsumer La interface Supplier
Interfaces funcionales y lambda expresiones 116 public interface Supplier<E> { E get(); } Ejemplo: Supplier<Integer> s = Random::nextInt; Los interfaces Comparable y Comparator: orden natural y ordenes altenativos Hemos visto cómo definir un orden natural en un tipo. La interfaz del tipo T debe extender a la interfaz Comparable<T>, y en la clase hay que implementar el método compareTo. Este método es un método individual: public interface Comparable<E> { int compareTo(E other; } La interface funcional Comparator<T> representa el tipo funcional 𝑇𝑇𝑇𝑇× 𝑇𝑇𝑇𝑇→𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖. Comparable y Comparator están pensados para representar órdenes: el primero se concreta mediante un método individual y representa el orden natural del tipo si lo tiene, el segundo representa un orden alternativo y se concreta mediante lambda expresiones normalmente o métodos ofrecidos por la propia interface. La interface Comparator incluye los métodos estáticos siguientes: Fundamentos de programación: JAVA Miguel Toro Bonilla Los interfaces Comparable y Comparator: orden natural y ordenes altenativos
Interfaces funcionales y lambda expresiones 117 public interface Comparator<T> { int compare(T o1, T o2); static <T,U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor); static <T extends Comparable<? super T>> Comparator<T> naturalOrder(); default Comparator<T> reversed(); static <T extends Comparable<? super T>> Comparator<T> reverseOrder(); default Comparator<T> thenComparing(Comparator<? super T> other); . . . } Métodos de Comparator<T>: • comparing(Function<? super T,? extends U> keyExtractor: Genera un orden definido por los valores de la función keyExtractor aplicada a los objetos de tipo T. • naturalOrder(): El orden natural del tipo que debe tenerlo para usar este método. • reverseOrder(): El inverso del orden natural del tipo que debe tenerlo para usar este método. • thenComparing(Comparator<? super T> other): Combina dos órdenes aplicando primero uno y luego otro. Ejemplos: Comparator<Vuelo> comparadorVueloDestino = Comparator.comparing(Vuelo::getDestino); Comparator<Vuelo> comparadorVueloOcupacion = Comparator.comparing( x -> 100. * x.getNumPasajeros() / x.getNumPlazas()); Fundamentos de programación: JAVA Miguel Toro Bonilla
118 Métodos de factoría de streams os streams, como el resto de los objetos se crean con factorías. Las más relevantes son: • IntStream IntStream.range(a,b): Stream de int en un rango. • LongStream LongStream.range(a,b): Stream de long en un rango. • IntStream string.chars(): Stream con los valores enteros de los caracteres del string • Stream<E> collection.stream(): Stream con los objetos de la colección. • Stream<E> Arrays.stream(E[] elements): Stream con los objetos del array • Stream<E> Stream.iterate(E initialValue, Predicate<E> hashNext, UnaryOperator<E> next): Stream cuyos elementos se forman a partir de uno primero al que se aplica un operador binario mientras que se cumpla el predicado. • Stream<String> Files.lines(Path file): Un Stream con las líneas de un fichero L Fundamentos de programación: JAVA Miguel Toro Bonilla Métodos de factoría de streams
Métodos de factoría de streams 119 class FileTools { … public static Stream<String> fromFile(String file) { Stream<String> r = null; try { r = Files.lines(Paths.get(file), Charset.defaultCharset()); } catch (IOException e) { throw new IllegalArgumentException( "No se ha encontrado el fichero " + file); } return r; } IntStream, LongStream y DoubleStream tienen disponibles los métodos aritméticos sum y average que no están disponibles es Stream. El método boxed() de IntStream, LongStream y DoubleStream las transforma en Stream<Integer>, Stream<Long> y Stream<Double> respectivamente. Fundamentos de programación: JAVA Miguel Toro Bonilla
120 Operaciones sobre streams as operaciones sobre streams pueden ser de dos tipos: operaciones intermedias (o transformadoras y terminales (o acumuladoras). Las operaciones intermedias producen un stream a partir de otro. Es el caso, por ejemplo, de la operación filter. Las operaciones terminales producen un objeto que no es de tipo stream. Por ejemplo, el número de elementos del stream devuelto por el método count. Hay muchas operaciones aplicables sobre streams. En este tema veremos las siguientes: • Operaciones intermedias: o distinct o filter o flatMap o map o sorted • Operaciones terminales: o allMatch o anyMatch o average o collect o count o forEach o max o min o sum L Fundamentos de programación: JAVA Miguel Toro Bonilla Operaciones sobre streams
Operaciones sobre streams 121 Cada uno de estos métodos puede tener como parámetro un objeto de un tipo funcional. Métodos transformadores distinct(): Obtiene un nuevo stream sin elementos repetidos sorted(Comparator<T>) El método sorted permite, como su nombre indica, ordenar un stream. Para indicar el orden le pasamos como parámetro un comparador. Por ejemplo, supongamos que queremos ordenar los vuelos según su fecha de salida. Lo haríamos de la siguiente forma: getVuelos().stream() .sorted(Comparator.comparing(Vuelo::getFechaSalida)); map(Function<T,R>) El método map obtiene un stream de elementos de un tipo a partir de un stream de elementos de otro tipo. Para ello recibe como parámetro la función que transforma los objetos de un tipo en otro. En realidad existe una familia de métodos map, según el tipo de los objetos del stream de salida. Así, existen los métodos mapToInt, mapToLong, mapToDouble. Estos producen como resultado streams de tipos específicos IntStream, LongStream, DoubleStream que tienen métodos específicos. Veamos como ejemplo un método que obtiene el número total de pasajeros de los vuelos que salen en una fecha determinada. public IntStream numPasajerosVuelosFecha(LocalDate fecha) { return getVuelos().stream() .filter(x -> x.getFechaSalida().equals(fecha)) .mapToInt(x -> x.getNumPasajeros()); } flatMap(Function<T,Stream<R>) Fundamentos de programación: JAVA Miguel Toro Bonilla Métodos transformadores
Operaciones sobre streams 128 x -> x.setDestino(nuevoDestino)); representa la operación a realizar con los objetos de tipo vuelo, consistente en cambiar su destino al nuevo destino. Como vemos, el método no devuelve nada, sino que modifica los objetos del stream, en este caso los vuelos con el destino afectado por el temporal. Otros métodos de Stream Además de los métodos que hemos visto, existen otros métodos que se pueden aplicar sobre streams, como los siguientes: • findAny(). Método acumulador. Devuelve un objeto Optional que representa un elemento cualquiera del stream. • findFirst(). Método acumualdor. Devuelve un objeto Optional que representa el primer elemento del stream. • limit(Long). Método intermedio. Devuelve un stream con el tamaño máximo indicado. • peek(Consumer). Método intermedio. Aplica una acción a todos los elementos del stream, devolviendo el stream original. Nótese la diferencia respecto a forEach, que es un método terminal y aplica la acción al stream sin devolver nada. Operaciones adicionales de Stream • Similar al zip de Python <L,R,T> Stream<T> zip(Stream<L> left, Stream<R> right, BiFunction<L, R, T> combiner); • Similar al enumerate de Python <E> Stream<Enumerate<E>> enumerate(Stream<E> stream, Integer start); <E> Stream<Enumerate<E>> enumerate(Stream<E> stream); • Tipo Enumerate<E>: Propiedades • Counter: Integer • Value: E Fundamentos de programación: JAVA Miguel Toro Bonilla Otros métodos de Stream Operaciones adicionales de Stream
129 Lectura y escritura de ficheros y streams ava dispone de una clase Files con un conjunto de métodos estáticos para operar con ficheros y directorios. Veamos cómo realizar las operaciones básicas de lectura y escritura utilizando esta clase. Lectura de un fichero Para leer un fichero se utiliza el método lines: static Stream<String> lines(Path path); Este método puede disparar excepciones por lo cual lo encapsulamos, como habíamos comentado arriba, en el método: public static Stream<String> fromFile(String file) { Stream<String> r = null; try { r = Files.lines(Paths.get(file), Charset.defaultCharset()); } catch (IOException e) { throw new IllegalArgumentException( "No se ha encontrado el fichero " + file); } return r; } Una posible llamada a este método sería: J Fundamentos de programación: JAVA Miguel Toro Bonilla Lectura y escritura de ficheros y streams Lectura de un fichero
Lectura y escritura de ficheros y streams 130 Stream<String> s = fromFile("res/vuelos.txt"); Escritura en un fichero Para la escritura en ficheros utilizaremos el método write de la clase Files. Este método escribe los elementos de un iterable de String en un fichero de texto, a razón de uno por línea. Como este método puede disparar excepciones lo encapsulamos en: void writeStream(Stream<String> s, String file) { Iterable<String> it = ()->s.iterator(); try { Files.write(Paths.get(file),it); } catch (IOException e) { System.out.println(e.toString()); } } Fundamentos de programación: JAVA Miguel Toro Bonilla Escritura en un fichero
131 Versiones imperativas del código funcional Factoría IntStream IntStream.range(a,b); for(Integer i=a, i<b; i++) { …. } IntStream string.chars(); for(Integer i=a, i< string.length();i++) { Character c = string.charAt(i); …. } Stream<E> collection.stream(); for(E e: collection){ …. } Stream<E> Arrays.stream(E[] elements); for(Integer i=a; i< elements.length; i++) { E e = elements[i]; …. } Stream<E> Stream.iterate(E initialValue, Predicate<E> hasNext, UnaryOperator<E> next); Fundamentos de programación: JAVA Miguel Toro Bonilla Versiones imperativas del código funcional Factoría
Versiones imperativas del código funcional 132 for(E e=initialValue; hasNext(e);e = next(e)) { …. } Stream<String> Files.lines(Path file); List<String> lineas(String file){ List<String> lineas = null; try { BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); lineas = bufferedReader.lines() .collect(Collectors.toList()); bufferedReader.close(); } catch (IOException e) { System.out.println(e.toString()); } return lineas; } Funciones de transformación Stream<R> map(Function<E,R> f); for(E e: collection){ R r = f(e); … } Stream<R> flatMap(Function<E,Stream<R>> f); for(E e: collection){ for(R r: f(e)){ … } } Stream<E> filter(Predicate<E> p); Fundamentos de programación: JAVA Miguel Toro Bonilla Funciones de transformación
Versiones imperativas del código funcional 133 for(E e: collection){ if(p(e)){ … } } Acumuladores Long count() Integer a = 0; for(E e: collection){ a = a+1; } Double/Integer/Long sum(); Integer a = 0; for(E e: collection){ a = a+e; } Double average(); Double a = 0; Integer n = 0; for(E e: collection){ a = a+e; n = n+1; } return a/n; Boolean allMatch(Predicate<E> p); Boolean a = true; for(E e: collection){ a = p(e); if(!a) break; } return a; Boolean anyMatch(Predicate<E> p); Fundamentos de programación: JAVA Miguel Toro Bonilla Acumuladores
Versiones imperativas del código funcional 134 Boolean a = false; for(E e: collection){ a = p(e); if(a) break; } return a; Optional<E> filter(p).findFirst(); E a = null; for(E e: collection){ if(p(e)) { a = e; break; } } return Optional.ofNullable(a); Optional<E> min(Comparator<E>); Optional<E> max(Comparator<E>); E a = null; for(E e: collection){ if(a == null || e < a) { // con el comparador a = e; } } return Optional.ofNullable(a); Optional<E> max(Comparator<E>); Optional<E> max(Comparator<E>); E a = null; for(E e: collection){ if(a == null || e > a) { // con el comparador a = e; } } return Optional.ofNullable(a); Optional<E> reduce(BinaryOperator<E> bo); Fundamentos de programación: JAVA Miguel Toro Bonilla
Versiones imperativas del código funcional 135 E a = null; for(E e: collection){ if(a == null) { a = e; } else { a = bo(a,e); } } return Optional.ofNullable(a); List<E> collect(Collectors.toList()); List<E> a = new ArrayList<>(); for(E e: collection){ a.add(e); } return a; Set<E> collect(Collectors.toSet()); Set<E> a = new HashSet<>(); for(E e: collection){ a.add(e); } return a; String collect(Collectors.joining(sp,pf,sf)); // aplicado a Stream<String> String a = pf; Boolean primero = true; for(E e: collection){ if(primero){ a = a+e; primero = false; } else { a = a +sp+e; } } return a+sf; Map<K,List<E>> collect(Collectors.groupingBy(Function<E,K) fkey); Fundamentos de programación: JAVA Miguel Toro Bonilla
Versiones imperativas del código funcional 136 Map<K,List<E>> a = new HashMap<>(); for(E e: collection){ K key = fkey(e); if(!a.keySet().contains(key)){ a.put(key, new ArrayList<>()); } m.get(key).add(e); } return a; Map<K,Set<E>> collect(Collectors.groupingBy(Function<E,K) key, Collectors.toSet())); Map<K,Set<E>> a = new HashMap<>(); for(E e: collection){ K key = fkey(e); if(!a.keySet().contains(key)){ a.put(key, new HashSet<>()); } a.get(key).add(e); } return a; SortedMap<K,Set<E>> collect(Collectors.groupingBy( Function<E,K) key, ()->new TreeMap<>( Comparator.reverseOrder()), Collectors.toSet())); SortedMap<K,Set<E>> a = new TreeMap<>( Comparator.reverseOrder()); for(E e: collection){ K key = fkey(e); if(!a.keySet().contains(key)){ a.put(key, new HashSet<>()); } a.get(key).add(e); } return a; Map<K,Long> collect(Collectors.groupingBy( Function<E,K) key, Collectors.counting ())); Fundamentos de programación: JAVA Miguel Toro Bonilla
Versiones imperativas del código funcional 137 Map<K,Long> a = new HashMap<<(); for(E e: collection){ K key = fkey(e); Long r = 1L+ a.getOrDefault(key,0L); a.put(key,r); } return a; Map<K,Integer> collect(Collectors.groupingBy(Function<E,K) key, Collectors.collectingAndThen( Collectors.counting (), Long::intValue))); Map<K,Integer> a = new HashMap<<(); for(E e: collection){ K key = fkey(e); Integer r = 1+ a.getOrDefault(key,0); a.put(key,r); } return a; Map<K, Double > collect(Collectors.groupingBy(Function<E,K) fkey, Collectors.summingDouble( Function<E,Double) fsum))); Map<K,Double> a = new HashMap<<(); for(E e: collection){ K key = fkey(e); Integer r = fsum(e)+a.getOrDefault(key,0); a.put(key,r); } return a; Map<K, List<T>> collect(Collectors.groupingBy(Function<E,K) fkey, Collectors.mapping(Function<E,T) fm, Collectors.toList())); Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 144 public static Double distancia(Coordenadas2D c1, Coordenadas2D c2) { Double radio_tierra = 6373.0; Coordenadas2D c1R = c1.toRadians(); Coordenadas2D c2R = c2.toRadians(); Double incLat = c2R.latitud-c1R.latitud; Double incLong = c2R.longitud-c1R.longitud; Double a = Math.pow(Math.sin(incLat/2),2)+ Math.cos(c1R.latitud) *Math.cos(c2R.latitud)* Math.pow(Math.sin(incLong/2),2); Double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return radio_tierra*c; } public static Coordenadas2D center( List<Coordenadas2D> coordenadas) { Double averageLat = coordenadas.stream() .mapToDouble(x->x.latitud).average() .getAsDouble(); Double averageLng = coordenadas.stream() .mapToDouble(x->x.longitud) .average().getAsDouble(); return Coordenadas2D.of(averageLat,averageLng); } @Override public String toString() { return String.format("(%.2f,%.2f)", this.latitud,this.longitud); } } Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 145 record Coordenadas3D(Double latitud,Double longitud, Double altitud) { public static Coordenadas3D of(Double latitud, Double longitud, Double altitud) { return new Coordenadas3D(latitud, longitud, altitud); } public Double distancia(Coordenadas3D c) { return distancia(this,c); } public Coordenadas2D to2D() { return Coordenadas2D.of(latitud, longitud); } public static Double distancia(Coordenadas3D c1, Coordenadas3D c2) { Coordenadas2D c12D = Coordenadas2D.of(c1.latitud, c1.longitud); Coordenadas2D c22D = Coordenadas2D.of(c2.latitud, c2.longitud); return Math.sqrt(Math.pow( Coordenadas2D.distancia(c12D,c22D),2) +Math.pow(c1.altitud-c1.altitud,2)); } @Override public String toString() { return String.format("(%f,%f,%f)", latitud,longitud,altitud); } } Destacamos los detalles del método toString() para conseguir el formado adecuado. Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 146 record Marca(LocalTime time, Coordenadas3D coordenadas) { public static Marca parse(String text) { String[] campos = text.split(","); LocalTime time = LocalTime.parse(campos[0], DateTimeFormatter.ofPattern("HH:mm:ss")); Coordenadas3D coordenadas = Coordenadas3D.of(Double.parseDouble(campos[1]), Double.parseDouble(campos[2]), Double.parseDouble(campos[3])/1000); return Marca.of(time, coordenadas); } public static Marca of(LocalTime time, Coordenadas3D coordenadas) { return new Marca(time, coordenadas); } @Override public String toString() { return String.format("(%s,%.2f,%.2f,%.2f)", time,coordenadas.latitud(), coordenadas.longitud(),coordenadas.altitud()); } } Destacamos los detalles de la función parse y el uso del parsing de los datos del tiempo. Igualmente el formato de salida para estos datos. record Intervalo(Marca principio, Marca fin) { public static Intervalo of(Marca principio, Marca fin) { return new Intervalo(principio, fin); } @Override public String toString() { return String.format("(%s,%s)", this.principio.toString(),this.fin.toString()); } public Double desnivel() { return this.fin.coordenadas().altitud()- this.principio().coordenadas().altitud(); } Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 147 public Double longitud() { return this.principio().coordenadas() .distancia(this.fin().coordenadas()); } public Double tiempo() { return this.principio().time() .until(this.fin().time(), ChronoUnit.SECONDS)/3600.; } public Double velocidad() { return this.longitud()/this.tiempo(); } } Del tipo Ruta queremos hacer dos implementaciones: una funcional y otra imperativa. Para concretar los detalles diseñaremos un interface Ruta con los métodos relevantes del tipo y dos clases RutaF y RutaI con las implementaciones funcional e imperativa respectivamente. Las dos clases RutaF y RutaI pueden tener aspectos comunes que sería interesante programar una sola vez. Para conseguirlo diseñamos la clase abstracta RutaA de la que heredarán las dos anteriores. Por esa razón deben ser clases y no records. Esta clase implementará Ruta y los métodos cuyo código puede ser compartido entre RutaF y RutaI. Veamos en primer lugar el interface Ruta y la clase abstracta RutaA. Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 148 public interface Ruta { public static Ruta of(List<Marca> marcas) { return switch(RutaA.tipo) { case Funcional->RutaF.of(marcas); case Imperativa->RutaI.of(marcas); }; } public static Ruta leeDeFichero(String fichero) { return switch(RutaA.tipo) { case Funcional->RutaF.leeDeFichero(fichero); case Imperativa->RutaI.leeDeFichero(fichero); }; } Double getTiempo(); Double getLongitud(); Double getVelocidadMedia(); Intervalo getIntervalo(Integer i); Double getDesnivelCrecienteAcumulado(); Double getDesnivelDecrecienteAcumulado(); } public abstract class RutaA implements Ruta { public static enum TipoImplementacion{Imperativa,Funcional} public static TipoImplementacion tipo = TipoImplementacion.Funcional; protected List<Marca> marcas; protected RutaA(List<Marca> marcas) { this.marcas = marcas; } @Override public Double getVelocidadMedia() { return this.getLongitud()/this.getTiempo(); } @Override public Intervalo getIntervalo(Integer i) { return Intervalo.of(this.marcas.get(i), this.marcas.get(i+1)); } } Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 149 En la clase abstracta RutaA se incluyen algunos métodos comunes a las dos implementaciones. public class RutaF extends RutaA implements Ruta { public static Ruta of(List<Marca> marcas) { return new RutaF(marcas); } private RutaF(List<Marca> marcas) { super(marcas); } public static Ruta leeDeFichero(String fichero) { List<Marca> marcas = FileTools.streamFromFile("ficheros/ruta.csv") .map(x->Marca.parse(x)) .collect(Collectors.toList()); return of(marcas); } Veamos ahora la implementación del resto de propiedades. public Double getTiempo() { Integer n = this.marcas.size(); return this.marcas.get(0).time() .until(this.marcas.get(n-1).time(), ChronoUnit.SECONDS)/3600.; } public Double getLongitud() { Integer n = this.marcas.size(); return IntStream.range(0,n-1).boxed() .map(i->this.intervalo(i)) .mapToDouble(x->x.longitud()) .sum(); } @Override public String toString() { return marcas.stream().map(m->m.toString()) .collect(Collectors.joining("\n")); } Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 150 public Double desnivelCrecienteAcumulado() { Integer n = this.marcas.size(); return IntStream.range(0, n-1) .boxed() .map(i->this.intervalo(i)) .filter(e->e.desnivel()>0) .mapToDouble(e->e.longitud()) .sum(); } public Double desnivelDecrecienteAcumulado() { Integer n = this.marcas.size(); return IntStream.range(0, n-1) .boxed() .map(i->this.intervalo(i)) .filter(e->e.desnivel()<0) .mapToDouble(e->e.longitud()) .sum(); } La implementación imperativa sigue el mismo esquema que la funcional. Diseñamos un constructor y dos métodos de factoría. public class RutaI extends RutaA implements Ruta { public static Ruta of(List<Marca> marcas) { return new RutaI(marcas); } public static Ruta leeDeFichero(String fichero) { List<Marca> marcas = new ArrayList<>(); for (String linea:FileTools.lineasFromFile( "ficheros/ruta.csv")) { Marca m = Marca.parse(linea); marcas.add(m); } return of(marcas); } private RutaI(List<Marca> marcas) { super(marcas); } Las propiedades se implementan ahora siguiendo el patrón imperativo. Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 151 @Override public Double getLongitud() { Integer n = this.marcas.size(); Double a = 0.; for(Integer i=0; i< n-1; i++) { Intervalo it = this.getIntervalo(i); Double ln = it.longitud(); a = a + ln; } return a; } @Override public Double getTiempo() { Integer n = this.marcas.size(); Double a = 0.; for(Integer i=0; i< n-1; i++) { Intervalo it = this.getIntervalo(i); Double ln = it.tiempo(); a = a + ln; } return a; } @Override public Double getDesnivelCrecienteAcumulado() { Integer n = this.marcas.size(); Double a = 0.; for(Integer i=0; i< n-1; i++) { Intervalo it = this.getIntervalo(i); if (it.desnivel()>0) { Double ln = it.longitud(); a = a + ln; } } return a; } Fundamentos de programación: JAVA Miguel Toro Bonilla
Diseño de rutas 152 @Override public Double getDesnivelDecrecienteAcumulado() { Integer n = this.marcas.size(); Double a = 0.; for(Integer i=0; i< n-1; i++) { Intervalo it = this.getIntervalo(i); if (it.desnivel()<0) { Double ln = it.longitud(); a = a + ln; } } return a; } @Override public String toString() { String r = marcas.get(0).toString(); for(int i = 1;i<marcas.size();i++) { r += "\n"+marcas.get(i).toString(); } return r; } Fundamentos de programación: JAVA Miguel Toro Bonilla
153 Servicio de biclicletas de sevilla e dispone de los datos de las estaciones de la red Servicio de biclicletas de sevilla (Sevici). Los datos se encuentran en un fichero CSV. Cada línea del fichero contiene seis datos: • Nombre de la estación • Número total de bornetas de la estación • Número de bornetas vacías • Número de bicicletas disponibles • Latitud • Longitud Los datos dependen del instante en que se obtiene el fichero, ya que el número de bicicletas y bornetas libres varía de forma continua. Estan serían, por ejemplo, las primeras líneas del fichero en un momento dado: name,slots,empty_slots,free_bikes,latitude,longitude 149_CALLE ARROYO,20,11,9,37.397829929383,-5.97567172039552 257_TORRES ALBARRACIN,20,16,4,37.38376948792722,-5.908921914235877 243_GLORIETA DEL PRIMERO DE MAYO,15,6,9,37.380439481169994,- 5.953481197462 109_AVENIDA SAN FRANCISCO JAVIER,15,1,14,37.37988413609134,- 5.974382770011 073_PLAZA SAN AGUSTIN,15,10,4,37.38951386231434,-5.984362789545622 Los principales aspectos que tendremos que resolver a la hora de procesar estos datos de entrada serán saltar la línea de encabezado del fichero, separar adecuadamente los campos mediante las comas e S Fundamentos de programación: JAVA Miguel Toro Bonilla Servicio de biclicletas de sevilla