Está en la página 1de 30

Estructura de Datos

Jaime Salvador
Version v1.0.0
Tabla de contenidos
Lista de figuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Lista de código. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1. Genéricos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1. Búsqueda en un arreglo de elementos Integer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2. Búsqueda en un arrego de elementos Persona . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3. Búsqueda genérica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3.1. Gestionar valores no encontrados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4. Encontrar el mínimo y máximo de un arreglo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.5. Leer datos de un archivo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.5.1. Instanciar un vector genérico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.5.2. Crear instancias de un tipo genérico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.5.3. Implementación completa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2. Interfaces funcionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.1. Gradle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.2. Constructores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3. Creación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.4. Operaciones de filtrado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.4.1. first . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.4.2. last . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.4.3. take . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.4.4. takeLast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.4.5. skip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.4.6. skipLast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.4.7. filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.5. Operaciones condicionales y booleanas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.5.1. all. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.5.2. any . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.5.3. contains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.5.4. takeWhile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.5.5. skipWhile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1

Lista de figuras
Figura 1. Tipos de datos comparables
Figura 2. Algoritmo leer datos de un archivo de texto CSV
Figura 3. Interfaz RowParser
Figura 4. ArrayDataset

Lista de código
Listado 1. Búsqueda de valores enteros
Listado 2. Búsqueda de objetos tipo Persona
Listado 3. Búsqueda con Genéricos
Listado 4. Búsqueda con Genéricos y tipo de dato opcional
Listado 5. Máximo y mínimo de un arreglo
Listado 6. Lectura de un archivo de texto separado por comas (,)

Jaime Salvador M.
2 1. Genéricos

1. Genéricos
En esta sección se consideran los siguientes ejercicios:

• Buscar de un elemento dentro de un arreglo. El método debe retornar el elemento buscado y


el índice en el que se encuentra.
• Encontrar el valor mínimo y máximo dentro de un arreglo.

1.1. Búsqueda en un arreglo de elementos Integer


Por ejemplo, dado el arreglo [7,5,2,3,6,8,4,5,0], se pide buscar el valor 6. El método debe
retornar dos valores (6,4), el valor 6 es el elemento buscado y el valor 4 es el índice en que se
encuentra.

Listado 1. Búsqueda de valores enteros

//Record utilizado para almacenar el elemento buscado y el índice


record EnteroPos(Integer valor, Integer index) {

class BuscarEnteros {

public static EnteroPos buscarInt(Integer[] datos, Integer elem) {


for(int i=0;i<datos.length;i++) {
if(datos[i].equals(elem)) {
return new EnteroPos(datos[i],i);
}
}

return null;
}

public static void main(String[] args) {


Integer[] datos = {7,5,2,3,6,8,4,5,0};

var ret = buscarInt(datos,6);

System.out.printf("Valor=%s en la posición %d",


ret.valor(),
ret.index()
);
}
}

1.2. Búsqueda en un arrego de elementos Persona


Si se tienen los datos de personas representados por la siguiente clase:
3

//Record que representa a una persona


record Persona(Integer id, String name, String address, Integer age) {
}

Escribir el método que permita buscar una Persona en base a su ID. Por ejemplo, si se tiene el
arreglo de personas:

[
[1, "nombre1", "dir1", 10],
[2, "nombre2", "dir2", 15],
[3, "nombre3", "dir3", 23],
[4, "nombre4", "dir4", 18],
[5, "nombre5", "dir5", 14]
]

buscar la persona con ID=3. El método debe retornar la Persona y el índice en el que se
encuentra: ([3, "nombre3", "dir3", 23], 2) el valor [3, "nombre3", "dir3", 23]
corresponde a la Persona con ID=3 y el valor 2 corresponde al índice dentro del vector.

//Record utilizado para almacenar el elemento buscado y el índice


record PersonaPos(Persona valor, Integer index) {
}

class BuscarPersonas {

static PersonaPos buscarPersona( Persona[] datos, Integer id ) {


for(int i=0;i<datos.length;i++) {
if(datos[i].id().equals(id)) {
return new PersonaPos(datos[i],i);
}
}

return null;
}

public static void main(String[] args) {


Persona[] datos = {
new Persona(1, "nombre1","dir1", 10 ),
new Persona(2, "nombre2","dir2", 15 ),
new Persona(3, "nombre3","dir3", 23 ),
new Persona(4, "nombre4","dir4", 18 ),
new Persona(5, "nombre5","dir5", 14 )
};

var ret = buscarPersona(datos, 3);

System.out.printf("Persona=%s en la posición %d",


ret.valor(),
ret.index()
);
}
}

La salida del programa anterior es la siguiente:

Jaime Salvador M.
4 1.2. Búsqueda en un arrego de elementos Persona

Persona=Persona[id=3, name=nombre3, address=dir3, age=23] en la posición 2

El caso más completo implica buscar una persona en base a una condición general representada
por Predicate<T>, para esto el método de búsqueda debe recibir el arreglo de personas así como
el predicado a aplicar, es decir:

PersonaPos buscarPersona( Persona[] datos, Predicate<Persona> condicion )

Con estas variaciones, el método general para buscar personas en base a una condición arbitraria
es el siguiente:

Listado 2. Búsqueda de objetos tipo Persona

import java.util.function.Predicate;

class BuscarPersonas {

static PersonaPos buscarPersona( Persona[] datos,


Predicate<Persona> cond ) ❶
{
for(int i=0;i<datos.length;i++) {
if( cond.test(datos[i]) ) {
return new PersonaPos(datos[i],i);
}
}

return null;
}

public static void main(String[] args) {


Persona[] datos = {
new Persona(1, "nombre1","dir1", 10 ),
new Persona(2, "nombre2","dir2", 15 ),
new Persona(3, "nombre3","dir3", 23 ),
new Persona(4, "nombre4","dir4", 18 ),
new Persona(5, "nombre5","dir5", 14 )
};

var ret = buscarPersona(datos, per->per.id().equals(3)); ❷

System.out.printf("%s en la posición %d",


ret.valor(),
ret.index()
);
}
}

❶ Método para buscar pesonas


❷ Invocación del método pasando como segundo argumento un predicado que filtra la persona
con ID=3

La salida del programa anterior es la siguiente:

Persona[id=3, name=nombre3, address=dir3, age=23] en la posición 2


5

1.3. Búsqueda genérica


De los ejemplso anteriores, la búsqueda de valores enteros y la búsqueda de personas comparten
algunas clases y parte del algoritmo:

• Las dos búsquedas necesitan clases para almacenar el valor retornado IntegerPos para la
búsqueda de valores enteros y PersonaPos para la búsqueda de personas.
• El algoritmo de iteración sobre los datos es el mismo, básicamente un for que itera sobre los
datos, el chequeo de una condición y el retorno del resultado.

El valor de retorno se lo puede representar como una clase genérica con dos parámetros:

public record Pair<T,U>(T first, U second) {


public static <T,U> Pair<T,U> of(T v1, U v2) {
return new Pair<>(v1,v2);
}
}

En el caso de la búsqueda de valores enteros, los parémetros son Integer, Integer:


Pair<Integer,Integer>

En el caso de la búsqueda de personas, los parémetros son Persona, Integer:


Pair<Persona,Integer>

El algoritmo se lo puede generalizar de la siguiente forma:

public static Pair<tipos> buscar(arreglo, condicion) {


for(int i=0;i<data.length;i++) {
if( condicion ) {
return Pair.of(valores);
}
}

return null;
}

El método de búsqueda puede ser generalizado para un arreglo de cualquier tipo (por ejemplo T) y
la condición se la puede generalizar utilizando un predicado que chequea elementos del vector, en
este caso Predicate<T>. De esta forma, el método genérico de búsqueda es el siguiente:

Listado 3. Búsqueda con Genéricos

import java.util.function.Predicate;

public class BuscarPersonas2 {


public static <T> Pair<T,Integer> buscar(T[] data, Predicate<T> cond) {
for(int i=0;i<data.length;i++) {
if( cond.test(data[i]) ) {
return Pair.of(data[i], i);
}
}

return null;
}

Jaime Salvador M.
6 1.3. Búsqueda genérica

public static void main(String[] args) {


Persona[] datos = {
new Persona(1, "nombre1","dir1", 10 ),
new Persona(2, "nombre2","dir2", 15 ),
new Persona(3, "nombre3","dir3", 23 ),
new Persona(4, "nombre4","dir4", 18 ),
new Persona(5, "nombre5","dir5", 14 )
};
var ret = buscar(datos, per->per.id().equals(3));

System.out.printf("%s en la posición %d",


ret.first(),
ret.second()
);
}
}

 Cómo se puede gestionar de mejor forma cuando el valor buscado no se


encuentra dentro del vector?

1.3.1. Gestionar valores no encontrados


En el caso que el valor buscado no se encuentre dentro del arreglo, la implementación de la
sección anterior retorna el valor null. En este caso es remendable utilizar una de las opciones
siguientes:

Generar una excepción

Generar una excepción en caso que el valor no se encuentre:

public static <T> Pair<T,Integer> buscar(T[] data, Predicate<T> cond) {


for(int i=0;i<data.length;i++) {
if( cond.test(data[i]) ) {
return Pair.of(data[i], i);
}
}

throw new RuntimeException("Valor no encontrado"); ❶


}

❶ Se genera una excepción en caso que el valor buscado no se encuentre en el arreglo

Retornar un tipo opcional

Utilizar el tipo de dato Optiona<T> como valor de retorno del método buscar:

public static <T> Optional<Pair<T,Integer>> buscar(T[] data,


Predicate<T> cond) {
for(int i=0;i<data.length;i++) {
if( cond.test(data[i]) ) {
return Optional.of(Pair.of(data[i], i)); ❶
}
}

return Optional.empty(); ❷
7

❶ Se retorna un valor opcional con el elemento encontrado


❷ Se retorna un opcional vacío en caso que el valor buscado no se encuentre en el arreglo

A continuación se presenta el programa completo utilizando la segunda opción:

Listado 4. Búsqueda con Genéricos y tipo de dato opcional

public class Buscar {


public static <T> Optional<Pair<T,Integer>> buscar(T[] data,
Predicate<T> cond) {
for(int i=0;i<data.length;i++) {
if( cond.test(data[i]) ) {
return Optional.of( Pair.of(data[i], i) );
}
}

return Optional.empty();
}

public static void main(String[] args) {

//-- buscar en un vector de personas


Persona[] personas = {
new Persona(1, "nombre1", "dir1", 10),
new Persona(2, "nombre2", "dir2", 15),
new Persona(3, "nombre3", "dir3", 23),
new Persona(4, "nombre4", "dir4", 18),
new Persona(5, "nombre5", "dir5", 14),
};

var optPersona = buscar(personas, p -> p.id().equals(3));

if (optPersona.isPresent()) {
var data = optPersona.get();

System.out.printf("%s, índice=%s\n", data.first(), data.second());


} else {
System.out.println("Persona no encontrada");
}
}
}

 Cómo se puede utilizar el método buscar para buscar un valor dentro de un


arreglo de enteros?

1.4. Encontrar el mínimo y máximo de un arreglo


Para encontrar el valor mínimo dentro de un vector se debe realizar los siguientes pasos:

• Asumir como valor mínimo el primer elemento del arreglo: minimo=datos[0]


• Iterar sobre todos los elementos del vector y verificar si el elemento actual es menor que el
mínimo: elemento actual<minimo
◦ Si el valor es menor, asignar minimo=elemento actual

Jaime Salvador M.
8 1.4. Encontrar el mínimo y máximo de un arreglo

◦ Continuar con la iteración

 Un proceso similar se puede aplicar para encontrar el máximo dentro de un


arreglo.

De la descripción anterior, es necesario que los elementos del vector sean comparables (menor
que, mayor que). Los tipos de datos numéricos (tipos wrappers) son comparables, sin embargo
tipos de datos personalizados como la clase Persona no son comparables por defecto.

Si definimos el método static <T> Pair<T,T> minMax(T[] data), es necesario que el tipo T sea
un tipo comparables, esto es T es un/una Comparable.

Tomando nuevamente el ejemplo del arreglo de personas:

[
[1, "nombre1", "dir1", 10],
[2, "nombre2", "dir2", 15],
[3, "nombre3", "dir3", 23],
[4, "nombre4", "dir4", 18],
[5, "nombre5", "dir5", 14]
]

Podemos buscar la persona que tiene la edad más baja (mínimo) y la persona que tiene la edad
más alta (máximo), en este caso el concepto de Comparar Personas se reduce a comparar las
edades; esto es: una persona es menor que otra (o mayor) si la edad es menor (o mayor).

Si definimos la interface Comparable, podemos tener la siguiente jerarquía de clases:

Figura 1. Tipos de datos comparables

 La interface Comprable es parte del lenguaje Java. Adicionalmente, los arreglos


que contienen elementos comparables pueden ser ordenados.

Con la jerarquía anterior, la búsqueda del mínimo y máximo es posible siempre que el arreglo
contega objetos que implementen la interface Comparable (que sean comparables). Por lo tanto, el
tipo de dato T del método minMax debe extender/implementar la interface comparable:

static <T extends Comparable> Pair<T,T> minMax(T[] data).

A continuación se presenta el programa completo que implementa el método minMax:


9

Listado 5. Máximo y mínimo de un arreglo

record Persona(Integer id, String name, String address, Integer age)


implements Comparable<Persona>{
@Override
public int compareTo(Persona o) {
return this.age.compareTo(o.age);
}
}

public class MinmaxMain {

public static <T extends Comparable> Pair<T,T> minMax(T[] data) {


T min = data[0];
T max = data[0];

for(int i=0;i<data.length;i++) {
if(data[i].compareTo(min)<0) {
min = data[i];
}
if(data[i].compareTo(max)>0) {
max= data[i];
}
}

return Pair.of(min,max);
}

public static void main(String[] args) {

//-- buscar en un vector de personas


Persona[] personas = {
new Persona(1, "nombre1","dir1", 10 ),
new Persona(2, "nombre2","dir2", 15 ),
new Persona(3, "nombre3","dir3", 23 ),
new Persona(4, "nombre4","dir4", 18 ),
new Persona(5, "nombre5","dir5", 14 ),
};

//-- buscar el min/max del vector de personas


var mm = minMax(personas);

System.out.printf("Mínimo=%s, máximo=%s\n", mm.first(), mm.second() );


}
}

1.5. Leer datos de un archivo


En esta sección se desarrolla un programa que permite leer archivos de texto, en formato CSV
(Comma Separated Values), el cual contiene un registro de datos por cada fila.

A manera de ejemplo se muestra la información parcial del archivo iris.data:

150 ❶
5.1,3.5,1.4,0.2,Iris-setosa ❷
4.9,3.0,1.4,0.2,Iris-setosa

Jaime Salvador M.
10 1.5. Leer datos de un archivo

4.7,3.2,1.3,0.2,Iris-setosa
4.6,3.1,1.5,0.2,Iris-setosa
..

❶ Número de elementos en el archivo (150 en este ejemplo)


❷ Registros contenidos en el archivo. Cada elemento del registro está separado por una , (coma)

Cada línea del archio anterior contiene la siguiente información:

• sepal length in cm (real)


• sepal width in cm (real)
• petal length in cm (real)
• petal width in cm (real)
• class (string):
◦ Iris Setosa
◦ Iris Versicolour
◦ Iris Virginica

Para representar esta información se crea la clase IrisRow la cual contiene atributos asociados a
la información descrita anteriormente.

El agoritmo para leer el archivo es el siguiente:


11

Figura 2. Algoritmo leer datos de un archivo de texto CSV

A continuación se presenta el programa que permite leer el archivo iris.data:

//clase que representa una fila en el archivo


record IrisRow(double sLength, double sWidth, double plength,
double pWidth, String cls) {

public class LeerArchivo {

static IrisRow[] read(String fileName) {

Jaime Salvador M.
12 1.5. Leer datos de un archivo

try (var brd = new BufferedReader(new FileReader(fileName))) { ❶


String line = brd.readLine(); ❷
int size = Integer.valueOf(line);

IrisRow[] ret = new IrisRow[size]; ❸

int index = 0;
for (int i = 0; i < size; i++) { ❹
// leer una línea del archivo
line = brd.readLine(); ❺

if (!line.isBlank()) {
//separar las palabras utilizando `,`
var tokens = line.split(","); ❻

IrisRow item = new IrisRow( ❼


Double.valueOf(tokens[0]),
Double.valueOf(tokens[1]),
Double.valueOf(tokens[2]),
Double.valueOf(tokens[3]),
tokens[4]
);

ret[index] = item; ❽
index++;
}
}

return ret; ❾
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}

❶ Abrir el archivo a leer


❷ Leer la primera línea que contiene el número de registros
❸ Crear el vector en el cual se almacenarán los datos
❹ Iterar size veces (una por cada registro)
❺ Leer una línea del archivo
❻ Separar la línea en palabras utilizando ,
❼ Crear una instancia de IrisRow para representar un registro del archivo
❽ Agregar el registro al vector
❾ Retornar el vector con los datos

Las líneas del código marcadas con 1, 2, 4, 5, 6, 8 y 9 son generales y no dependen del tipo de
información contenida en el archivo. Por otro lado, las líneas del código marcadas con 3 y 7 son
particulares y dependen del tipo de información contenida en el archivo.

A continuación se muestra la utilización del método LeerArchivo.read:

public static void main(String[] args) {


String fname = "c:/files/iris.data";
13

IrisRow[] data1 = LeerArchivo.read(fname);

//imprimir los datos


for (var item : data1) {
System.out.println(item);
}
}

Si escribimos el programa de una manera más general, se tendría:

public class LeerArchivo {

static TIPO[] read(String fileName) { ❶

try (var brd = new BufferedReader(new FileReader(fileName))) {


String line = brd.readLine();
int size = Integer.valueOf(line);

TIPO[] ret = ...❷

int index = 0;
for (int i = 0; i < size; i++) {
// leer una línea del archivo
line = brd.readLine();

if (!line.isBlank()) {
//separar las palabras utilizando `,`
var tokens = line.split(",");

TIPO item = ... ❸

ret[index] = item;
index++;
}
}

return ret;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}

❶ El método retorna un vector de algún TIPO


❷ Crear el vector en el cual se almacenará la información
❸ Crear un ítem para almacenar en el vector

En la solución general presentanda anteriormente, es necesario considerar:

1. TIPO representa un dato genérico que puede ser de tipo IrisRow o cualquier otro tipo
dependiendo del archivo de datos (punto 1)
2. Es necesario instanciar un vector cuyos elementos son genéricos (punto 2)
3. Es necesario crear instancia del tipo genérico (punto 3)

Jaime Salvador M.
14 1.5. Leer datos de un archivo

1.5.1. Instanciar un vector genérico


No es posible utilizar el operador new para instanciar un vector genérico, en su lugar es necesario
utilizar la clase Array y el método newInstance. El método tiene la siguiente forma:

public static Object newInstance(Class<?> componentType, int length)

Donde:

• Class<?> componentType representa la clase asociada al tipo genérico


• int length representa el tamaño del vector

class DemoArrayGenerico {
static <T> T[] crearVector(Class<T> cls, int size) {
T[] arr = (T[] )Array.newInstance(cls, size); ❶

//hacer algo con el arreglo

return arr;
}

public static void main(String[] args) {


//arreglo de Integer, tamaño 10
Integer[] arr1 = crarVector(Integer.class, 10);

//arreglo de IrisRow, tamaño 10


IrisRow[] arr2 = crearVector(IrisRow.class, 10);
}
}

❶ Creación de un arrego de genéricos

Para todas las clases en Java, es posible determiar el tipo de dato utilizando la
 sintaxis NombreClase.class, por ejemplo Integer.class, String.class,
IrisRow.class.

Utilizando lo indicado anteriormente, podemos rescribir el algoritmo de lectura del archivo dela
siguiente forma:

public class LeerArchivo {

static <T> T[] read(String fileName, Class<T> cls) { ❶

try (var brd = new BufferedReader(new FileReader(fileName))) {


String line = brd.readLine();
int size = Integer.valueOf(line);

T[] ret = (T[] )Array.newInstance(cls, size); ❷

int index = 0;
for (int i = 0; i < size; i++) {
// leer una línea del archivo
line = brd.readLine();

if (!line.isBlank()) {
15

//separar las palabras utilizando `,`


var tokens = line.split(",");

TIPO item = ... ❸

ret[index] = item;
index++;
}
}

return ret;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}

❶ El método utiliza el genérico T y retorna un arreglo T[]


❷ Creación del arreglo de genéricos
❸ Pendiente: creación de las instancias de la clase genérica

1.5.2. Crear instancias de un tipo genérico


Similar a los arreglos de genéricos, no es posible utilizar el operador new para instanciar un
genérico.

En el método para leer un archivo, en la sección marcada con 3, es necesario realizar lo siguiente:

1. Crear una instancia del tipo de dato T


2. Iniciaizar la instancia con los valores leídos del archivo y que se encuentran en el vector
String[] tokens = line.split(",")

var tokens = line.split(",");

IrisRow item = new IrisRow(


Double.valueOf(tokens[0]),
Double.valueOf(tokens[1]),
Double.valueOf(tokens[2]),
Double.valueOf(tokens[3]),
tokens[4]
);

ret[index] = item;

Es posible crear instancias de tipo genérico utilizando la clase asociada al tipo (Integer.class,
String.class, etc):

class DemoCrearGenerico {
static <T> T crearGenerico(Class<T> cls) {

try {
T obj = cls.getConstructor() ❶
.newInstance();
return obj;

Jaime Salvador M.
16 1.5. Leer datos de un archivo

} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) throws Exception {


Integer obj1 = crearGenerico(Integer.class);

IrisRow obj2 = crearGenerico(IrisRow.class);

IrisRow obj3 = IrisRow.class.getConstructor()


.newInstance();
}
}

❶ Creación de la istancia genérica

En el ejemplo anterior, el método getConstructor() retorna el contructor que


no recibe ningún parámetro (constructor canónico) y lo invoca utilizando el
 método newInstance(). Por esta razón, es necesario que todas las clases
utilizadas en el ejemplo anterior tengan un constructor canónico (sin
parámetros).

El código anterior permite crear instancias de tipos genéricos asumiendo que la clase tiene un
constructor por defecto (sin parámetros). Luego de crear la istancia, es necesario fijar los valores
de cada uno de los atributos. Es posible fijar los valores de los atributos de una clase utilizando
reflexión[1], sin embargo es una tarea más complicada que la creación de uns instancia.

En lugar de crear instancias de tipos genéricos, una solución más simple es abstraer la acción de
creación e inicialización de la instancia a través de una interface que la llamaremos RowParser<T>.

Figura 3. Interfaz RowParser

Para realizar la creación e inicialización de la instancia, es posible pasar una implementación de


RowParser<T> al método read

public class LeerArchivo {

static <T> T[] read(String fileName, Class<T> cls, RowParser<T> parser) {

try (var brd = new BufferedReader(new FileReader(fileName))) {


String line = brd.readLine();
int size = Integer.valueOf(line);

T[] ret = (T[] )Array.newInstance(cls, size);

int index = 0;
17

for (int i = 0; i < size; i++) {


// leer una línea del archivo
line = brd.readLine();

if (!line.isBlank()) {
//separar las palabras utilizando `,`
var tokens = line.split(",");

T item = parser.parse(tokens); ❶

ret[index] = item;
index++;
}
}

return ret;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}

❶ Creación de las instancias de la clase genérica

1.5.3. Implementación completa


A continuación se presenta el código completo de la lectura d eun archivo de texto:

Listado 6. Lectura de un archivo de texto separado por comas (,)

public class LeerArchivo {

static <T> T[] read(String fileName, Class<T> cls, RowParser<T> parser) {

try (var brd = new BufferedReader(new FileReader(fileName))) {


String line = brd.readLine();
int size = Integer.valueOf(line);

T[] ret = (T[] )Array.newInstance(cls, size);

int index = 0;
for (int i = 0; i < size; i++) {
// leer una línea del archivo
line = brd.readLine();

if (!line.isBlank()) {
//separar las palabras utilizando `,`
var tokens = line.split(",");

T item = parser.parse(tokens);

ret[index] = item;
index++;
}
}

Jaime Salvador M.
18 1.5. Leer datos de un archivo

return ret;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}

A manera de ejemplo, el siguiente código muestra la lectura del archivo iris.data:

record IrisRow(double sepalLength, double sepalWidth, double petalLength,


double petalWidth, String cls) {

public class TestIrisRowArray {

private static IrisRow toRow(String[] tokens) {


return new IrisRow(
Double.valueOf(tokens[0]),
Double.valueOf(tokens[1]),
Double.valueOf(tokens[2]),
Double.valueOf(tokens[3]),
tokens[4]
);
}

public static void main(String[] args) {


String fileName = "c:/kk/iris.data";

var dataset = ArrayDataset.fromFile(


fileName,
IrisRow.class,
TestIrisRowArray::toRow
);

for (var item : dataset) {


System.out.println(item);
}
}
}
19

2. Interfaces funcionales
En esta sección se generaliza el programa de lectura de un archivo de texto para generar un tipo
de dato denominado ArrayDataset el cual abstrae el proceso para trabajar con un conjunto de
datos almacenados en un arreglo.

La clase ArrayDataset proporciona métodos para construir instancias de la misma:

• A partir de un archivo de texto ArrayDataset.fromFile(String fileName, Class<T> cls,


RowParser<T> parser)
• A partir de una lista de valores fijos ArrayDataset.of(T[]… items)

A continuación se presenta el diagrama de clases correspondiente al ejemplo:

Figura 4. ArrayDataset

La clase ArrayDataset representa un arreglo de tamaño fijo y permite ejecutar varias acciones
sobre un arreglo de datos genérico:

• count() retorna el número de elementos en el arreglo.

Jaime Salvador M.
20 2. Interfaces funcionales

• first() retorna el primer elemento del arreglo.


• last() retorna el último elemento del arreglo.
• min() retorna el valor mínimo del arreglo, os elementos del arreglo deben ser comparables.
• max() retorna el valor máximo del arreglo, los elementos del arreglo deben ser comparables.
• minMax() retorna el los valores (mínimo,máximo) del arreglo, los elementos del arreglo deben
ser comparables.
• forEach(consumer) itera sobre los elementos del arreglo y aplica el consumer a cada
elemento.
• take(n) retorna un arreglo que contiene los primeros n-elementos.
• takeLast(n) retorna un arreglo que contiene los últimos n-elementos.
• takeWhile(condicion) descarta los elementos de un arreglo después de que una condición
especificada se convierta en falsa.
• skip(n) ignora los primeros n-elementos y retorna un arreglo con los elementos restantes.
• skipLast(n) ignora los últimos n-elementos y retorna un arreglo con los elementos restantes.
• skipWhile(condicion) descarta los elementos de un arreglo hasta que una condición
especificada se convierta en falsa
• filter(condicion) retorna un arreglo con todos los elementos que cumplen una condición.
• all(condicion) retorna true si todos los elementos del arreglo cumplen con la condición.
• any(condicion) retorna true si al menos un elemento del arreglo cumple con la condición.
• contains(condicion) retorna true si el arreglo contiene el elemento??
• findFirst(condicion) retorna el primer elemento del arreglo que cumple una condición.
• map(funcion) convierte cada elemento del arreglo a un tipo defindo y retorna un nuevo
arreglo con los nuevos elementos.
• zip(dataset, biFuncion) combina elementos de dos arreglos en un nuevo elemento, retorna
el arreglo resultante.

A manera de ejemplo, si se tiene los arreglos:

ds1=[1,2,3,4,5,6,7,8,9]
ds2=["uno","dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]

el resultado de las operaciones anteriores es:

ds1.count(): 9
ds1.first(): 1
ds1.last(): 9
ds1.min(): 1
ds1.max(): 9
ds1.minMax(): (1,9)
ds1.forEach(System.out::println)`: imprime todos los elementos
ds1.take(2): [1,2]
ds1.takeLast(2): [8,9]
ds1.takeWhile(it->it%2!=0): [1]
ds1.skip(2): [3,4,5,6,7,8,9]
ds1.skipLast(2): [1,2,3,4,5,6,7]
ds1.skipWhile(it->it%2!=0): [2,3,4,5,6,7,8,9]
ds1.filter(it->it%2==0): [2,4,6,8]
ds1.all(it->it>5): false
ds1.any(it->it>5): true
ds1.contains(it->it.equals(5)): true
ds1.findFirst(it->it.equals(5)): 5
ds1.map(it->"0"+it): [01,02,03,04,05,06,07,08,09]
ds1.zip(ds2, Pair::of):
21

[(first=1, second=uno),(first=2, second=dos),...,(first=9, second=nueve)]

2.1. Gradle
Para esta sección se utilizará un proyecto Gradle[2] con la finalidad de automatizar la gestió de
dependencias.

2.2. Constructores
La clase ArrayDataset encapsula las operaciones que s epueden realizar con un arreglo de datos,
hace uso de tipos genéricos para representar cualquier tipo de dato. Para evitar que la clase
ArrayDataset pueda ser instanciada directamente, la clase incluye dos constructores privados:

public class ArrayDataset<T> {


private T[] data; ❶
private Class<?> _class; ❷

private ArrayDataset(Class<?> _class, T[] data) { ❸


this._class = _class;
this.data = data;
}

private ArrayDataset(T[] data) { ❹


this._class = data[0].getClass();
this.data = data;
}
}

❶ Variable de instancia en la que se almacena la información del arreglo


❷ Variable de instancia que contiene el tipo de dato almaenado por el arreglo
❸ Constructor que inicializa la instancia con la clase (tipo de dato) y el arreglo (datos) que
almacena
❹ Constructor que inicializa la instancia con el arreglo (datos) que almacena

Debido a que los contrustores de la clase son privados, la única forma de crear instancias es a
través de los métodos estáticos fromFile y fromArray.

2.3. Creación
El método fromFile permite crear instancias de la clase a partir de información almacenada en
un archivo de texto separado por comas (,):

String fileName = "c:/data/iris.data";

var dataset = ArrayDataset.fromFile(


fileName,
IrisRow.class,
TestIrisRowArray::toRow
);

El método fromArray permite crear instancias de la clase a partir de un arreglo de datos, el


siguiente arreglo crea una instancia a partir de un arreglo de valores enteros:

Jaime Salvador M.
22 2.4. Operaciones de filtrado

var dataset = ArrayDataset.fromArray(1,2,3,4,5,6,7,8,9);

A continuación se presenta la implementación de los métodos descritos anteriormente.

public class ArrayDataset<T> {


// otros métodos

public static <T> ArrayDataset<T> fromFile( ❶


String fileName,
Class<T> cls,
RowParser<T> parser) {

try (var brd = new BufferedReader(new FileReader(fileName))) {


int index = 0;
String line = brd.readLine();

int size = Integer.parseInt(line);

T[] data = (T[]) Array.newInstance(cls, size);

for (int i = 0; i < size; i++) {


line = brd.readLine();

if (!line.isBlank()) {
var tokens = line.split(",");
T item = parser.parse(tokens);

data[index] = item;
index++;
}
}

return new ArrayDataset<>(cls, data);


} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static <T> ArrayDataset<T> fromArray(T... items) { ❷


return new ArrayDataset<>(items);
}
}

❶ fromFile inicializa la instancia leyendo los datos desde un archivo


❷ fromArray inicializa la instancia a partir de un arreglo de datos

2.4. Operaciones de filtrado


Las operaciones de filtrado de datos son:

• first()
• last()
• take(n)
• takeLast(n)
23

• skip(n)
• skipLast(n)
• filter(condicion)

2.4.1. first
Retorna el primer elemento del conjunto de datos.

public T first() {
return data[0];
}

La implementación del método retorna el primer elemento (índice 0) del arreglo.

2.4.2. last
Retorna el último elemento del conjunto de datos.

public T last() {
return data[data.length - 1];
}

La implementación del método retorna el último elemento (índice length-1) del arreglo.

2.4.3. take
Retorna los primeros n-elementos del arreglo y genera una nueva instancia de la clase
ArrayDataset.

public ArrayDataset<T> take(int count) {


if(count>this.data.length) {
throw DatasetExceptionsUtils.createEx(
"take(%d): %d debe ser menor que %d",
count, count, this.data.length);
}

T[] tmpData = (T[]) Array.newInstance(_class, count); ❶

for (int i = 0; i < count; i++) { ❷


tmpData[i] = this.data[i];
}

return new ArrayDataset<>(this._class, tmpData); ❸


}

❶ Se crea un nuevo arreglo que contiene count-elementos. El arreglo contiene el mismo tipo de
elementos que el vector original (genérico T)
❷ Se copian los primeros count-elementos elementos al nuevo arreglo
❸ Se crea una nueva instancia de la clase ArrayDataset

Jaime Salvador M.
24 2.4. Operaciones de filtrado

2.4.4. takeLast
Retorna los últimos n-elementos del arreglo y genera una nueva instancia de la clase
ArrayDataset.

public ArrayDataset<T> takeLast(int count) {


if(count>this.data.length) {
throw DatasetExceptionsUtils.createEx(
"takeLast(%d): %d debe ser menor que %d",
count, count, this.data.length);
}

int size = count;

T[] tmpData = (T[]) Array.newInstance(_class, size); ❶

int index = 0;
for (int i = this.data.length-size; i <this.data.length;i++) { ❷
tmpData[index++] = this.data[i];
}

return new ArrayDataset<>(this._class, tmpData); ❸


}

❶ Se crea un nuevo arreglo que contiene count-elementos. El arreglo contiene el mismo tipo de
elementos que el vector original (genérico T)
❷ Se copian los últimos count-elementos al nuevo arreglo
❸ Se crea una nueva instancia de la clase ArrayDataset

2.4.5. skip
Ignora los primeros n-elementos del arreglo y retorna un nuevo arreglo con los elementos
restantes. Genera una nueva instancia de la clase ArrayDataset.

public ArrayDataset<T> skip(int count) {


if(count>this.count()) { ❶
return new ArrayDataset<>(this._class,
(T[] )Array.newInstance(_class, 0)
);
}

int size = this.data.length-count;

T[] tmpData = (T[]) Array.newInstance(_class, size); ❷

int index = 0;
for (int i = count; i < this.data.length; i++) { ❸
tmpData[index] = this.data[i];
index++;
}

return new ArrayDataset<>(this._class, tmpData); ❹


}

❶ Si el número de elementos a ignorar es mayor que el tamaño del arreglo, se retorna un arreglo
vacío
25

❷ Se crea un nuevo arreglo que contiene length-count-elementos. El arreglo contiene el mismo


tipo de elementos que el vector original (genérico T)
❸ Se copian los últimos count-elementos elementos al nuevo arreglo
❹ Se crea una nueva instancia de la clase ArrayDataset

2.4.6. skipLast
Ignora los últimos n-elementos del arreglo y retorna un nuevo arreglo con los elementos restantes.
Genera una nueva instancia de la clase ArrayDataset.

public ArrayDataset<T> skipLast(int count) {


if(count>this.data.length) { ❶
return new ArrayDataset<>(this._class,
(T[] )Array.newInstance(_class, 0)
);
}

int size = this.data.length-count;

T[] tmpData = (T[]) Array.newInstance(_class, size); ❷

int index = 0;
for (int i = 0; i <size;i++) { ❸
tmpData[index++] = this.data[i];
}

return new ArrayDataset<>(this._class, tmpData); ❹


}

❶ Si el número de elementos a ignorar es mayor que el tamaño del arreglo, se retorna un arreglo
vacío
❷ Se crea un nuevo arreglo que contiene length-count-elementos. El arreglo contiene el mismo
tipo de elementos que el vector original (genérico T)
❸ Se copian los últimos count-elementos elementos al nuevo arreglo
❹ Se crea una nueva instancia de la clase ArrayDataset

2.4.7. filter
Copia todos los elementos de un arreglo que cumplem una condición especificada. Genera una
nueva instancia de la clase ArrayDataset. A manera de ejemplo, si se tiene elarreglo:

datos = [1,5,7,4,2,3,5,8]

La llamada al método filter(esImpar) retornará el arreglo [1,5,7,3,5], es decir copia todos los
elementos del arreglo que cumplen la condición (esImpar).

A continuación, se presenta la implementación del método.

public ArrayDataset<T> filter(Predicate<T> pred) {


// contar los elementos
int count = 0; ❶
for (T it : data) {
if (pred.test(it)) {
count++;

Jaime Salvador M.
26 2.5. Operaciones condicionales y booleanas

}
}

T[] tmpData = (T[]) Array.newInstance(_class, count); ❷

int index = 0;
for (T it : data) { ❸
if (pred.test(it)) {
tmpData[index] = it;
index++;
}
}

return new ArrayDataset<>(this._class, tmpData); ❹


}

❶ Se determina el número de elementos que cumplen con la condición


❷ Se crea un nuevo arreglo que contiene count-elementos. El arreglo contiene el mismo tipo de
elementos que el vector original (genérico T)
❸ Se copian los elementos que cumplen la condición al nuevo arreglo
❹ Se crea una nueva instancia de la clase ArrayDataset

2.5. Operaciones condicionales y booleanas


Las operaciones de filtrado de datos son:

• all(condicion)
• any(condicion)
• contains(condicion)
• takeWhile(condicion)
• filter(condicion)

2.5.1. all
Retorna true si todos los elementos del arreglo cumplen con la condición.

public Boolean all(Predicate<T> pred) {


for(var it:data) {
if(pred.test(it)==false) {
return false;
}
}
return true;
}

2.5.2. any
Retorna true si al menos un elemento del arreglo cumple con la condición.

public Boolean any(Predicate<T> pred) {


for(var it:data) {
if(pred.test(it)) {
return true;
27

}
}
return false;
}

2.5.3. contains
Retorna true si el arreglo contiene el elemento.

public Boolean contains(Predicate<T> pred) {


for(var it:data) {
if(pred.test(it)) {
return true;
}
}
return false;
}

2.5.4. takeWhile
Descarta los elementos de un arreglo después de que una condición especificada se convierta en
falsa. Genera una nueva instancia de la clase ArrayDataset. A manera de ejemplo, si se tiene
elarreglo:

datos = [1,5,7,4,2,3,5,8]

La llamada al método takeWhile(esImpar) retornará el arreglo [1,5,7], es decir descarta los


elementos del arreglo a partir del primer elemento que no es impar (que no cumple la condición).
O, lo que es lo mismo, copia los elementos del vector original mientras se cumpla una condición
(esImpar).

A continuación, se presenta la implementación del método.

public ArrayDataset<T> takeWhile(Predicate<T> pred) {


// contar los elementos
int count = 0; ❶
for (T it : data) {
if (pred.test(it)) {
count++;
} else {
break;
}
}

T[] tmpData = (T[]) Array.newInstance(_class, count); ❷

int index = 0;
for (T it : data) { ❸
if (pred.test(it)) {
tmpData[index] = it;
index++;
} else {
break;
}

Jaime Salvador M.
28 2.5. Operaciones condicionales y booleanas

return new ArrayDataset<>(this._class, tmpData); ❹


}

❶ Se determina el número de elementos que cumplen con la condición


❷ Se crea un nuevo arreglo que contiene count-elementos. El arreglo contiene el mismo tipo de
elementos que el vector original (genérico T)
❸ Se copian los últimos count-elementos al nuevo arreglo
❹ Se crea una nueva instancia de la clase ArrayDataset

2.5.5. skipWhile
Descarta los elementos de un arreglo hasta que una condición especificada se convierta en falsa.
Genera una nueva instancia de la clase ArrayDataset. A manera de ejemplo, si se tiene elarreglo:

datos = [1,5,7,4,2,3,5,8]

La llamada al método skipWhile(esImpar) retornará el arreglo [4,2,3,5,8], es decir descarta


los elementos del arreglo mientras sean impares (mientras se cumple la condición). O, lo que es lo
mismo, copia los elementos del vector original a partir que la condición se vuelve falsa (esImpar).

A continuación, se presenta la implementación del método.

public ArrayDataset<T> skipWhile(Predicate<T> pred) {

int count = 0; ❶
while(count<this.data.length && pred.test(this.data[count])) {
count++;
}

int size = this.data.length-count;

T[] tmpData = (T[]) Array.newInstance(_class, size); ❷

int index = 0;
for(int i=count;i<this.data.length;i++) { ❸
tmpData[index++] = this.data[i];
}

return new ArrayDataset<>(this._class, tmpData); ❹


}

❶ Se determina el número de elementos que cumplen con la condición


❷ Se crea un nuevo arreglo que contiene count-length-elementos. El arreglo contiene el mismo
tipo de elementos que el vector original (genérico T)
❸ Se copian los elementos a partir del índice count al nuevo arreglo
❹ Se crea una nueva instancia de la clase ArrayDataset

[1] www.oracle.com/technical-resources/articles/java/javareflection.html
[2] gradle.org

También podría gustarte