| Fundamentos de Programación |
| Estudio del uso de Java como lenguaje de la asignatura |
En este documento os presento los resultados de un modesto estudio acerca de las posibilidades de usar Java como lenguaje base de la asignatura Fundamentos de Programación, y en que medida se pueden esconder aquellas características poco deseables en un primer lenguaje. Ya sabeis mi opinión de que la orientación a objeto (un pequeño subconjunto de ella) no debería ser una de esas características, pero aún así he intentado ocultarla lo más posible.
Para intentar conseguir los objetivos anteriores he definido varias clases, siendo la principal SimpleIO, que los alumnos deberían importar estaticamente en sus programas. Las otras tres clases tienen que ver con el tratamiento de ficheros y son TextFile, BinaryFile y GeneralFile.
Este documento debe considerarse como un borrador, con partes que no han sido completadas y otras que están mal escritas. Las clases que proporciono no se han probado adecuadamente. Por todo ello pido disculpas anticipadas por los errores que puedan existir, y solicito vuestra colaboración para corregirlos.
De los tipos primitivos de Java propongo usar el subconjunto formado por boolean byte char int double.
Respecto a Pascal las principales diferencias son que no existen tipos cuyo rango dependa del compilador, y que no existen subrangos ni enumerados (en realidad si, los han añadidos en la última versión, pero no me parece excesivamente importante su inclusión).
La asignación se realiza como en C, con el operador =, lo cual es muy desafortunado debido a su confusión con el operador de igualdad. Por suerte, y gracias a que Java dispone del tipo booleano y a la ausencia de conversiones automáticas a entero, en la mayoría de los casos (aunque no en todos) esta confusión provoca un error de compilación.
En Java es posible declarar variables en cualquier parte del código, no sólo al principio. También es posible declarar variables cuyo ámbito sea el de una estructura de control, no el del subprograma (se usa mucho en los bucles for: for(int i = 0; i < n; i++) {...}). Habría que decidir si esto se permite o no (yo por mi parte no veo ningún problema).
Las diferencias relevantes entre Pascal y Java respecto a los operadores son:
Como un argumento extra a favor de usar dot notation, en cualquier entorno avanzado como por ejemplo NetBeans, al escribir el punto se muestra una lista de los métodos disponibles junto con una descripción de su uso:

La principal diferencia entre C y Java es la no existencia (al igual que en Pascal) de conversiones numéricas automáticas en las que se pueda perder información.
int i = 5; double d = 3.14; String cad1,cad2,cad3; cad1 = i; // * ERROR DE COMPILACION: NO ES UNA EXPRESION. cad2 = ""+i; // Correcta cad3 = "i = "+i+", d = "+d; // Correcta |
Si el uso de typecasting no parece adecuado, se pueden definir sin problemas nuevas funciones estáticas en la clase SimpleIO para llevar a cabo esas operaciones.
Utilidades definidas en SimpleIO:
| Conversión | ||
| char | toUpperCase(char letra) | Conversión a mayúsculas |
| char | toLowerCase(char letra) | Conversión a minúsculas |
| String | toUpperCase(char letra) | Conversión a mayúsculas |
| String | toLowerCase(char letra) | Conversión a minúsculas |
| int | cad2Int(String cad) | Conversión de cadena a entero |
| double | cad2Double(String cad) | Conversión de cadena a real |
| String | int2Cad(int valor) | Traducción de entero a cadena |
| String | double2Cad(double valor) | Traducción de real a cadena |
| String | strFmt(String cad, Object... args) | |
| Importación estática de la clase Math | ||
| double | random() | Genera valor aleatorio entre 0 y 1. |
| ... | ... | ... |
Las estructuras basicas de control de Java son las mismas que las de C, y respecto a Pascal la única diferencia reseñable reside en la distinta filosofía de la alternativa múltiple (case vs. switch).
Un punto que merece consideración es el de las directivas break, continue y return usadas para terminar abruptamente la ejecución de un bucle. Hace años pensaba que, siguiendo las directrices de la programación estructurada, no se debian enseñar a los alumnos (Pascal también dispone de ellas), pero ultimamente me estoy decantando por la posibilidad de legalizar su uso, ya que realmente permiten escribir un código más claro y cercano al algoritmo que se desea implementar. Voy a poner como ejemplo un clásico, el cálculo de si un número es o no primo:
boolean esPrimo(int n) {
boolean flag;
int d;
if(n < 1) {
flag = false;
} else if(n < 3) {
flag = true;
} else {
d = 2;
while(d < n && n%d != 0)
d++;
flag = d >= n
}
return(flag);
} |
boolean esPrimo(int n) {
boolean flag;
int d;
if(n < 1) {
flag = false;
} else {
flag = true;
for(d = 2; d < n; d++)
if(n%d == 0)
flag = false;
}
return(flag);
} |
boolean esPrimo(int n) {
boolean flag;
int d;
if(n < 1) {
flag = false;
} else if(n < 3) {
flag = true;
} else {
for(d = 2; d < n; d++)
if(n%d == 0) break;
flag = d >= n
}
return(flag);
} |
boolean esPrimo(int n) {
if(n < 1) return(false);
for(int d = 2; d < n; d++)
if(n%d == 0) return(false);
return(true);
} |
Dado el problema de detectar si un número es primo, considerando falso valores nulos o negativos, primos el 1 (criterio de los físicos) y el 2, y usando la definición clásica (no divisible por 2..n-1), muestro arriba cuatro posibles implementaciones (he llamado flag a la variable logica porque así lo hacen muchos alumnos).
En este punto es donde existen divergencias importantes entre Pascal y Java:
Es legítimo preguntarse si la ausencia del paso por variable, en el caso de tipos primitivos, supone una limitación seria cuando se quieran computar varios valores en una misma función. Las técnicas habituales para enfrentarse a este problema son:
procedure Rect2Polar(r,a: real; var x,y: real); begin x := r*cos(a); y := r*sin(a); end; |
static double rectX(double r, double a) {
return(r*cos(a));
}
static double rectY(double r, double a) {
return(r*sin(a));
} |
const
N = ...; { Número de filas }
M = ...; { Número de columnas }
type
TMatriz = array[1..N,1..M] of integer;
procedure posmin(mat: TMatriz; var fil,col: integer);
var i,j : integer;
begin
fil := 1; col := 1;
for i := 1 to N do
for j := 1 to M do
if mat[i,j] < mat[fil,col] then
begin
fil := i; col := j
end
end; |
static int posmin(int[][] mat) {
int n = length(mat);
int m = length(mat[0]);
int fil = 1, col = 1;
for(int i = 1; i < n; i++)
for(int j = 1; j < m; j++)
if(mat[i][j] < mat[fil][col]) {
fil = i; col = j;
}
return(fil*n+col);
}
public static void main(...) {
int[][] m = {{2,3,1},{5,0,4}};
int res = posmin(m);
int fil = res/length(m);
int col = res%length(m);
...
} |
type
TCoord = record
X,Y: real
end;
procedure girar(var C: TCoord;
ang: real);
var tmp : real;
begin
tmp := C.x;
C.x := C.x*cos(ang) + C.y*sin(ang);
C.y := tmp*sin(ang) - C.y*cos(ang)
end; |
// WARNING:
// ORIENTACION A OBJETO INCORRECTA
public class Ejemplo {
// Clase interna "simulando"
// ser un registro
class Coord { double x,y; }
void girar(Coord c, double ang) {
double tmp = c.x;
c.x = c.x*cos(ang) + c.y*sin(ang);
c.y = tmp*sin(ang) - c.y*cos(ang);
}
...
} |
// Clase "de verdad"
public class Coord {
private double x,y;
public Coord(double x,
double y) {
this.x = x;
this.y = y;
}
void girar(double ang) {
double t = x;
x = x*cos(ang) + y*sin(ang);
y = t*sin(ang) - y*cos(ang);
}
...
}
// Clase que utiliza a la anterior
public class Ejemplo {
public static void main(...) {
Coord c = new Coord(2.0,-1.0);
c.girar(PI/2);
...
}
} |
En la mayoría de las situaciones prácticas (me baso en la hoja de problemas de Prog. I) las dos primeras técnicas permiten sortear la ausencia de paso por variable. En el resto de casos habría que plantearse la estrategia más adecuada (yo me inclino por no plantear problemas que requieran inevitablemente el encapsulamiento).
La entrada/salida en Java plantea problemas porque este lenguaje tiene entre sus objetivos fundamentales la compatibilidad entre plataformas y dispositivos. Esto tiene como consecuencia que:
Por ejemplo, para leer un entero por teclado se necesita el siguiente código:
import java.io.*;
public class Ejemplo {
public static void main(String[] args) {
BufferedReadear br = new BufferedReader(new InputStreamReader(System.in));
try {
String lin = br.readLine();
int n = Integer.parseInt(lin);
} catch(IOException ex) {
...
}
}
}
|
Lo que puede asustar al más templado. Afortunadamente todo esto se puede encapsular de forma que (casi) toda esta complejidad quede oculta para el alumno. Para ello he creado una conjunto de funciones en la clase SimpleIO, y las clases representando tipos de ficheros: TextFile, BinaryFile y GeneralFile diseñadas con los siguientes objetivos (perfectamente cuestionables):
Usando esta librería, ahora la lectura de un entero se realizaría con el siguiente código:
import static uva.infor.soft.SimpleIO.*;
public class Ejemplo {
public static void main(String[] args) {
int n = readInt();
}
}
|
La clase SimpleIO define un objecto estático, Consola, de tipo TextFile, que contiene funciones para leer y escribir tipos primitivos y lineas de texto por pantalla y teclado. Para usar este objeto se debe utilizar dot-notation, pero he definido wrappers para evitar su uso si esa es la estrategia seguida. A continuación se enumeran las funciones definidas (tienen el mismo nombre en Consola que en los wrappers). Para una descripción detallada remito al JavaDoc de la clase.
| Lectura de datos | ||
| boolean | readBoolean() | Lee un valor booleano |
| char | readChar() | Lee un carácter |
| int | readInt() | Lee un valor entero |
| double | readDouble() | Lee un valor real |
| String | readLine() | Lee una linea completa |
| String | readKeyword() | Lee una "palabra" |
| Lectura omitida | ||
| void | skipChar() | Descarta el siguiente carácter |
| void | skipLine() | Pasa a la linea siguiente |
| Fin de linea | ||
| boolean | isEndOfLine() | Comprueba si se ha llegado al final de la linea |
| Escritura de datos | ||
| void | write(dato) | Escribe la traducción a texto del dato |
| void | writeln(dato) | Igual que la anterior, pasando a siguiente linea |
| void | writeFmt(cadena,argumentos) | Escritura con formato |
| Control de errores | ||
| int | status() | Tipo de error de la última operación |
| String | msgError() | Mensaje explicativo del error |
Detalles sobre algunas operaciones:
A continuación pongo un ejemplo bastante rebuscado:
import static uva.infor.soft.SimpleIO.*;
public class Ejemplo {
public static void main(String[] args) {
int dia,mes,año;
double peso;
String nombre;
boolean zurdo;
char sep;
writeln("Introduzca fecha de nacimiento (dd/mm/aaaa),"+
"nombre entre comillas, peso y si es zurdo o no:");
dia = readInt();
sep = readChar();
mes = readInt();
skipChar();
año = readInt();
nombre = readKeyword();
peso = readDouble();
// Ejemplo de comprobación
if(status() != NO_ERROR) {
write("Error al leer el peso: ");
writeln(msgError());
}
zurdo = readBoolean();
write("Ha usado el separador: ");
writeln(sep);
// Escritura formateada
writeFmt("Hola, %s.\nNació el: %d-%d-%d\nSu peso es %.3f kg.\nEs zurdo: %b\n",
nombre,dia,mes,año,peso,zurdo);
// Conversión formateada a String
String fmt = strFmt("Hola, %s. Nació el: %d-%d-%d. Su peso es %f. Es zurdo: %b",
nombre,dia,mes,año,peso,zurdo);
writeln(fmt);
}
}
|
Si se desea usar dot-notation basta añadir el prefijo Consola. antes de cada operación
Si ejecutais este programa vereis que el usuario puede introducir los datos separándolos por espacios, comas, retornos de linea, etc. sin problemas, y que como valor booleano acepta cualquier palabra (interpreta "yes", "true", "si", "ok" y "vale" como cierto, y cualquier otra cosa como falso).
Detalle: Java acepta carácteres unicode como parte de identificadores, por eso podemos escribir "año" como nombre de variable.
Los ficheros de texto se representan por la clase TextFile, y se han incorporado wrappers a SimpleIO para que no sea necesario instanciarla directamente ni usar dot-notation. Además de las funciones descritas en el apartado anterior (para las que existen también wrappers en SimpleIO), existen las siguientes:
| Creación, apertura y cierre de fichero | ||
| TextFile | TextFile(nomfich,modo) | Constructor de TextFile. Se indica la ruta y el modo de apertura. |
| TextFile | openTextFile(nomfich) | Wrapper en SimpleIO para apertura modo lectura. |
| TextFile | createTextFile(nomfich) | Wrapper en SimpleIO para apertura modo escritura. |
| void | close() | Cierre del fichero. |
| void | close(TextFile fich) | Wrapper en SimpleIO para cierre de fichero. |
| Detección de fin de fichero | ||
| boolean | isEndOfFile() | Comprueba final de fichero. |
| boolean | isEndOfFile(TextFile fich) | Wrapper en SimpleIO para comprobación de fin de fichero. |
Ejemplo: En el siguiente código se crea un fichero de texto con 10 lineas, cada una conteniendo tres números, y a continuación se lee el fichero mostrándolo por pantalla en el formato original.
import static uva.infor.soft.SimpleIO.*;
import uva.infor.soft.TextFile;
public class Ejemplo {
public static void main(String[] args) {
// Variable "fichero lógico"
TextFile fich;
int i;
// Apertura para escritura
fich = createTextFile("prueba.txt");
// Ejemplo de comprobación de errores
if(status(fich) != NO_ERROR) {
writeln(msgError(fich));
return;
}
// Escritura de los datos
for(i = 1; i < 11; i++)
writeFmt(fich,"%5d %5d %5d\n",i,i*i,i*i*i);
close(fich);
// Apertura para lectura
fich = openTextFile("prueba.txt");
// Lectura por lineas
while(!isEndOfFile(fich)) {
while(!isEndOfLine(fich)) {
i = readInt(fich);
write(i);
write(" ");
}
// Salto a la linea siguiente
skipLine(fich);
writeln("");
}
close(fich);
}
}
|
import static uva.infor.soft.SimpleIO.*;
import uva.infor.soft.TextFile;
public class Ejemplo {
public static void main(String[] args) {
// Variable "fichero lógico"
TextFile fich;
int i;
// Apertura para escritura
fich = new TextFile("prueba.txt",MODO_ESCRITURA);
// Ejemplo de comprobación de errores
if(fich.status() != NO_ERROR) {
writeln(fich.msgError());
return;
}
// Escritura de los datos
for(i = 1; i < 11; i++)
fich.writeFmt("%5d %5d %5d\n",i,i*i,i*i*i);
fich.close();
// Apertura para lectura
fich = new TextFile("prueba.txt",MODO_LECTURA);
// Lectura por lineas
while(!fich.isEndOfFile()) {
while(!fich.isEndOfLine()) {
i = fich.readInt();
Consola.write(i);
Consola.write(" ");
}
// Salto a la linea siguiente
fich.skipLine();
Consola.writeln("");
}
fich.close();
}
}
|
Ambos códigos hacen lo mismo. El de la izquierda está wrappeado y el de la derecha no.
La clase BinaryFile representa a un fichero binario que almacena una secuencia de datos del mismo tipo (equivalente a la construcción en Pascal file of tipo). Para lograr un acceso uniforme ha sido necesario usar genericidad, y por lo tanto al definir una variable de este tipo se debe añadir al identificador BinaryFile, entre corchetes angulares, el tipo de datos almacenado, teniendo en cuenta que si es un tipo primitivo se debe usar la clase asociada (boolean ↔ Boolean, char ↔ Character, int ↔ Integer, double ↔ Double). A la hora de leer y escribir datos, sin embargo, se pueden usar directamente valores primitivos gracias al autoboxing. A continuación se muestran ejemplos de la definición de un fichero de enteros, de arrays de enteros y de arrays bidimensionales de strings:
El uso de genericidad dificulta el definir wrappers estáticos, y por lo tanto para utilizar esta clase se debe usar dot-notation. Como alternativa se pueden usar ficheros genéricos.
He definido un iterador sobre esta clase para poder usar bucles for-each sobre ella.
Funcionalidad de la clase:
| Creación, apertura y cierre de fichero | ||
| BinaryFile<tipo> | BinaryFile(nomfich,modo) | Constructor de BinaryFile. Se indica la ruta y el modo de apertura. |
| void | close() | Cierre del fichero. |
| Detección de fin de fichero | ||
| boolean | isEndOfFile() | Comprueba final de fichero. |
| Lectura y escritura de datos | ||
| tipo | read() | Lee un dato del fichero (del tipo asociado) |
| void | write(tipo dato) | Escribe un dato del tipo asociado en el fichero |
Ejemplo: En el siguiente ejemplo se crea un fichero binario de números reales generados al azar y a continuación se abre para lectura y se calcula la media de los valores.
import static uva.infor.soft.SimpleIO.*;
import uva.infor.soft.BinaryFile;
import static java.lang.Math.*;
public class Ejemplo {
public static void main(String[] args) {
BinaryFile
|
import static uva.infor.soft.SimpleIO.*;
import uva.infor.soft.BinaryFile;
import static java.lang.Math.*;
public class Ejemplo {
public static void main(String[] args) {
BinaryFile
|
En el código de la derecha se muestra el uso de un bucle for-each para recorrer el contenido del fichero. En este apartado existe otro ejemplo de uso de ficheros binarios.
Nota: La función random está definida en java.lang.Math.
La clase GeneralFile representa a un fichero binario que almacena una secuencia de datos que pueden ser de tipos distintos. He considerado 3 posibles diseños para esta clase:
Se ha elegido la segunda opción, para evitar el uso de typecasting en tipos primitivos. De momento sólo se han implementado métodos para los tipos primitivos numéricos.
Funcionalidad de la clase:
| Creación, apertura y cierre de fichero | ||
| GeneralFile | GeneralFile(nomfich,modo) | Constructor de GeneralFile. Se indica la ruta y el modo de apertura. |
| GeneralFile | openGeneralFile(nomfich) | Wrapper en SimpleIO para apertura modo lectura. |
| GeneralFile | createGeneralFile(nomfich) | Wrapper en SimpleIO para apertura modo escritura. |
| void | close() | Cierre del fichero. |
| void | close(GeneralFile fich) | Wrapper en SimpleIO para cierre de fichero. |
| Detección de fin de fichero | ||
| boolean | isEndOfFile() | Comprueba final de fichero. |
| boolean | isEndOfFile(GeneralFile fich) | Wrapper en SimpleIO para comprobación de fin de fichero. |
| Lectura y escritura de datos | ||
| int | readInt() | Lectura de un entero. |
| real | readDouble() | Lectura de un real. |
| Object | readOther() | Lectura de un tipo no primitivo. |
| void | writeInt(int valor) | Escritura de un entero. |
| void | writeDouble(double valor) | Escritura de un real. |
| void | writeOther(Object valor) | Escritura de un tipo no primitivo. |
| Wrappers de lectura y escritura de datos | ||
| int | readInt(GeneralFile fich) | Lectura de un entero. |
| real | readDouble(GeneralFile fich) | Lectura de un real. |
| Object | readOther(GeneralFile fich) | Lectura de un tipo no primitivo. |
| void | writeInt(GeneralFile fich, int valor) | Escritura de un entero. |
| void | writeDouble(GeneralFile fich, double valor) | Escritura de un real. |
| void | writeOther(GeneralFile fich, Object valor) | Escritura de un tipo no primitivo. |
Ejemplos: En la parte izquierda el mismo problema del apartado anterior utilizando ficheros generales y wrappers (el fichero físico generado en el problema anterior sería válido para este problema). En la parte derecha un ejemplo de generación y lectura de un fichero con varios tipos de datos distintos.
import static uva.infor.soft.SimpleIO.*;
import uva.infor.soft.GeneralFile;
import static java.lang.Math.*;
public class Ejemplo {
public static void main(String[] args) {
GeneralFile fich;
int i;
double valor, suma;
// Creación y apertura del fichero
fich = createGeneralFile("datos.dat");
for(i = 0; i < 100; i++)
writeDouble(fich,random());
close(fich);
// Cálculo de la media del fichero
fich = openGeneralFile("datos.dat");
i = 0; suma = 0.0;
while(!isEndOfFile(fich)) {
valor = readInt(fich);
suma += valor;
i++;
}
close(fich);
writeFmt("Media = %.5f",suma/i);
}
}
|
import static uva.infor.soft.SimpleIO.*;
import uva.infor.soft.GeneralFile;
import static java.lang.Math.*;
public class Ejemplo {
public static void main(String[] args) {
GeneralFile fich;
int i, j, n;
int[][] matriz = {{1,2},{3,4},{5,6}};
int[][] otra;
String fecha;
// Creación del fichero
fich = createGeneralFile("multiple.dat");
// Escritura de datos diversos
writeOther(fich,"29/3/2010");
n = length(matriz);
writeInt(fich,n);
for(i = 0; i < n; i++)
writeOther(fich,matriz[i]);
close(fich);
// Lectura de los datos
fich = openGeneralFile("multiple.dat");
fecha = (String) readOther(fich);
n = readInt(fich);
otra = new int[n][];
for(i = 0; i < n; i++)
otra[i] = (int[]) readOther(fich);
close(fich);
}
}
|
En el ejemplo de la derecha se muestra un fichero donde se almacenan un String, un entero y las filas de una matriz de enteros (la matriz se podría haber escrito directamente), y luego se leen los datos reconstruyendo la matriz (he aprovechado para mostrar la construcción parcial de una matriz). Para tipos no primitivos es necesario typecasting.
En este apartado existe otro ejemplo de uso de ficheros generales.
La clase java.io.RandomAccessFile define métodos para trabajar con ficheros de forma que se pueden realizar las siguientes acciones, no permitidas en los tipos de ficheros anteriores:
Esto es posible porque esta clase esta restringida a tratar con ficheros físicos, no con un flujo de datos de tipo general.
Si se decide que es conveniente que los alumnos conozcan estos tipos de accesos, sería relativamente sencillo crear una clase del estilo de GeneralFile y los wrappers apropiados en SimpleIO.
Sería interesante el considerar si se incluye en el temario algún apartado de gestión de sistemas de archivos. Java dispone de la clase java.io.File que incluye practicamente todas las operaciones relevantes (obtener un array con el listado de archivos de un directorio, obtención de información sobre ficheros, creación, borrado y modificación de ficheros y directorios, etc.) y sería muy sencillo crear wrappers en SimpleIO para acceder a estas funciones.
La diferencia más importante entre Pascal/C y Java, considerando su uso en la asignatura, radica en la distinta filosofía que subyace al tratamiento de los tipos de datos "estructurados". Mientras que Pascal/C se basan en la composición explícita de representaciones internas de los datos (arrays de registros, arrays de arrays de strings, etc.), Java (como todos los lenguajes OO) usa un enfoque distinto: Los tipos "estructurados" son clases o interfaces que presentan funcionalidad, escondiendo la representación interna y relacionandose entre sí de otras formas (herencia, implementación de interfaces, etc.).
Afortunadamente Java tiene un tratamiento especial para los arrays, manteniendo la sintaxis habitual, pero siguen existiendo cambios en la forma de trabajar con tipos no primitivos, que paso a enumerar:
Las variables de tipo no primitivo almacenan referencias al contenido, no el propio contenido:
Los datos de tipo no primitivo deben crearse en tiempo de ejecución:
El uso de dot-notation
No existencia del tipo registro
En la clase SimpleIO se ha definido la función length(array) como un wrapper de la función del mismo nombre asociada a la clase array.
Respecto a la creación de arrays en Java en tiempo de ejecución en vez de estar ya definidos con longitud fija en tiempo de compilación, considero que debería respetarse la sintaxis Java, sin crear wrappers por las razones siguientes:
Nota: Tanto en C como en Pascal/Delphi se pueden crear arrays con tamaño dado en tiempo de ejecución: En C usando punteros y reserva de memoria y en Pascal/Delphi con arrays dinámicos y setLength.
Considero arrays dinámicos aquellos que pueden alterar su tamaño en tiempo de ejecución. En Pascal/Delphi existen (no se los damos en Programación I pero si en Estructuras de Datos). En Java existen dos posibilidades para usarlos:
int[] vec; vec = new int[100]; // ... se trabaja con vec hasta que es necesario ampliarlo ... int[] tmp = new int[200]; // Copia de los datos de vec a tmp arraycopy(vec,0,tmp,0,vec.length()); // Asignación de referencia (el vector original se descarta) vec = tmp; // Ahora vec tiene los datos originales y 100 posiciones extra ... |
Las diferencias básicas entre los tipos string entre Pascal/C y Java son:
He definido las siguientes funciones en SimpleIO para evitar el uso de dot-notation con strings:
| Wrappers sobre strings (inmutabilidad) | ||
| int | length(String cad) | Longitud del string. |
| char | charAt(String cad, int i) | Carácter en índice. |
| String | substring(String cad, int i0) | Obtener subcadena. |
| String | substring(String cad, int i0, int n) | Obtener subcadena. |
| boolean | strCompare(String cad1, String cad2) | Comparación de strings. |
| int | strIndexOf(String cad, char ch) | Búsqueda de carácter. |
| int | strIndexOf(String cad, int i0, char ch) | Búsqueda de carácter. |
| int | strIndexOf(String cad, String subcad) | Búsqueda de subcadena. |
| int | strIndexOf(String cad, int i0, String subcad) | Búsqueda de subcadena. |
Ejemplo: En el siguiente ejemplo se resuelve un problema del último examen de Programación I (ver enunciado, tercer problema). El objetivo del problema es detectar si dos cadenas de texto son anagramas. El algoritmo usado aquí consiste en crear vectores que indiquen el número de veces que aparece cada letra en cada cadena y comparar si son o no iguales (es mas un ejercicio de modularidad de arrays que de cadenas).
static boolean esLetra(char ch) {
return(ch >= 'A' && ch <= 'Z');
}
static void recuento(String cad, int[] vec) {
int i;
char ch;
for(i = 0; i < length(cad); i++) {
ch = charAt(cad,i);
if(esLetra(ch)) vec[ch-'A']++;
}
}
static boolean vectoresIguales(int[] vec1, int[] vec2) {
int i;
if(length(vec1) != length(vec2)) return false;
for(i = 0; i < length(vec1); i++)
if(vec1[i] != vec2[i]) return false;
return true;
}
static boolean esAnagrama(String cad1, String cad2) {
// Número de caracteres considerados letras
final int NUM_LETRAS = 'Z'-'A'+1;
// Vectores de recuento
int[] vec1 = new int[NUM_LETRAS];
int[] vec2 = new int[NUM_LETRAS];
// Contar apariciones de letras
recuento(cad1,vec1);
recuento(cad2,vec2);
// Comprobar si son iguales
return vectoresIguales(vec1,vec2);
} |
Si deseamos tener strings cuyo contenido se pueda cambiar, considero que existen tres estrategias:
He definido en la clase SimpleIO las siguientes funciones que usan la tercera estrategia:
| Wrappers sobre strings (mutabilidad) | ||
| String | setCharAt(String cad, int i, char ch) | Cambiar carácter. |
| String | strInsert(String cad, int i0, String subcad) | Insertar subcadena. |
| String | strInsert(String cad, int i0, char ch) | Insertar subcadena. |
| String | strDelete(String cad, int i0, int n) | Borrar subcadena. |
Ejemplo: El mismo problema del apartado anterior pero usando un algoritmo distinto: En este caso primero se eliminan los caracteres no letras de la segunda cadena, y a continuación se recorre la primera cadena, comprobando si cada letra de ella aparece en la segunda cadena (si no ya se sabe que no son anagramas) y eliminandola de la segunda cadena. Si la segunda cadena queda vacía, son anagramas.
Versión con StringBuilder, sin wrappers:
static boolean esLetra(char ch) {
return(ch >= 'A' && ch <= 'Z');
}
static boolean esAnagrama(String cad1, String cad2) {
// La segunda cadena se va a modificar, por lo que creamos un
// StringBuilder para trabajar sobre él.
StringBuilder cad = new StringBuilder(cad2);
// 1. Eliminamos todo lo que no sea letra de cad2
int i = 0;
while(i < cad.length())
if(esLetra(cad.charAt(i))) i++; else cad.deleteCharAt(i);
// 2. Recorremos cad1 y por cada letra buscamos su aparición en
// cad y la borramos
for(i = 0; i < cad1.length(); i++) {
char ch = cad1.charAt(i);
if(esLetra(ch)) {
int k = cad.indexOf(ch+"");
if(k > -1) cad.deleteCharAt(k); else return(false);
}
}
// 3. Comprobar si cad está vacía
return(cad.length() == 0);
} |
Versión con wrappers:
static boolean esLetra(char ch) {
return(ch >= 'A' && ch <= 'Z');
}
static boolean esAnagrama3(String cad1, String cad2) {
char ch;
int i = 0;
// 1. Eliminamos todo lo que no sea letra de cad2
while(i < length(cad2))
if(esLetra(charAt(cad2,i))) {
i++;
} else {
cad2 = strDelete(cad2,i,1);
}
// 2. Recorremos cad1 y por cada letra buscamos
// su aparición en cad2 y la borramos
for(i = 0; i < length(cad1); i++) {
ch = charAt(cad1,i);
if(esLetra(ch)) {
int k = strIndexOf(cad2,ch);
if(k > -1) {
cad2 = strDelete(cad2,k,1);
} else {
return(false);
}
}
}
// 3. Comprobar si cad2 está vacía
return(length(cad2) == 0);
} |
La forma natural de implementar el concepto de variable dinámica de Pascal es mediante la definición de clases. El problema que aparece aquí (caso identico al de los registros) es la regla OO de no acceder directamente a los atributos de un objeto. Si se enseña a crear constructores de clase (se les puede mentir y decir que son inicializadores) es relativamente facil crear ejemplos sencillos que ilustren el concepto. Por ejemplo, se puede mostrar una pila de una forma muy sencilla:
public class Ejemplo {
// Clase interna
static class Nodo {
int val;
Nodo sig;
// Constructor
public Nodo(int v, Nodo n) { val = v; sig = n; }
}
public static void main(String[] args) {
Nodo pila = null;
Nodo p;
int i;
// Creación de la pila
for(i = 1; i < 11; i++) pila = new Nodo(i,pila);
// Recorrido de valores
p = pila;
while(p != null) {
writeln(p.val);
p = p.sig;
}
} |
Ficheros y documentación de la librería
En construcción
En construcción
En este apartado se examinarán distintos enfoques para la resolución de la última práctica de la asignatura Programación I (ver enunciado).
En construcción
En construcción
Reutilizando la clase Mazo y Partida del apartado anterior, es posible dar un mini-curso guiado donde se reconvierta el programa en una versión utilizable en un teléfono movil.
Descarga de ficheros:
La forma más cómoda de instalarlo es enviarlo via Bluetooth al telefono movil desde un portatil.
César Vaca Rodríguez
2010 Departamento de Informática, Universidad de Valladolid.