Download 18 - Proyectos
Document related concepts
no text concepts found
Transcript
18: Proyectos Este capítulo incluye un conjunto de proyectos que se construyen en el material presentado en este libro o de otra manera no ajustada en capítulos anteriores. La mayor parte de estos proyectos son significativamente más complicados que los ejemplos en el resto de libro, y a menudo demuestran usos y técnicas nuevas de bibliotecas de clase. Procesar texto Si vienes de un C o C++ de fondo, podrías ser escéptico en primero del poder de Java en lo que se refiere a manejar texto. Ciertamente, un inconveniente es que la velocidad de ejecución es más lenta y eso podría entorpecer una cierta cantidad de tus esfuerzos. Sin embargo, las herramientas (en detalle la clase String) son muy poderosas, como los ejemplos en esta sección muestran (y las mejoras de desempeño han sido prometidas para Java). Como verás, estos ejemplos fueron creados para solucionar problemas que se levantaron en la creación de este libro. Sin embargo, no están restringidos para eso y las soluciones que le ofrecen fácilmente pueden ser adaptadas a otras situaciones. Además, demuestran el poder de Java en un área que previamente no ha sido enfatizado en este libro. Extrayendo listados de código Sin duda has notado que cada listado completo (no el fragmento de código) de código en este libro comienza y finaliza con marcas especiales de etiqueta de comentario '//:' y '///:~'. Esta meta información es incluida a fin de que el código pueda ser automáticamente extraído del libro en archivos de código fuente del compilable. En mi libro previo, tuve un sistema que me permitió automáticamente incorporarse archivos probados de código en el libro. En este libro, sin embargo, descubrí que fue a menudo más fácil pegar el código en el libro una vez que estaba inicialmente probado y, ya que es difícil de poner lo correcto la primera vez, para realizar ediciones para el código dentro del lib ro. ¿Pero cómo extraerlo y probar el código? Este programa es la respuesta, y podría venir bien cuando te dispones a solucionar un problema de procesamiento del texto. También demuestra muchas de las características de clase String. Primero guardo el libro entero en formato del texto de ASCII en un archivo separado. El programa CodePackager tiene dos modos (el cual puedes ver descrito en usageString ): Si usas la bandera -p, espera ver un archivo de entrada conteniendo el texto de ASCII del libro. Pasará a través de este archivo y usará las marcas de la etiqueta del comentario para extraer el código, y usa el nombre de archivo en la primera línea para determinar el nombre del archivo. Además, busca la declaración del paquete en el caso de que necesita colocar el archivo en un directorio especial (escogido por la ruta indicada por la declaración package). Pero eso no es todo. También espera el cambio en los capítulos siguiéndole la pista a los nombres del paquete. Ya que todos los paquetes para cada capítulo empiezan con c02, c03, c04, etc. para indicar el capítulo donde pertenecen (excepto por esos a partir de com, los cuales son ignorados con el objeto de seguirle la pista a los capítulos), con tal de que el primer listado en cada capítulo contenga una declaración package con el número de capítulo, el programa CodePackager puede seguir la pista de cuando el capítulo cambió y metió todos los subsiguientes archivos en e l subdirectorio nuevo chapter. Como cada archivo es extraído, es colocado dentro de un objeto SourceCodeFile que es luego colocado dentro de una colección. (Este proceso estará descrito más a fondo después.) Estos objetos SourceCodeFile simplemente podrían guardarse en archivos, pero eso nos trae para el segundo uso para este proyecto. Si invocas a CodePackager sin la bandera -p espera un archivo “empacado” como la entrada, que luego se extraerá en archivos separados. Así la bandera -p quiere decir que los archivos extraídos serán encontrados “empacados” en este único archivo. ¿Por qué la molestia con el archivo empacado? Porque las plataformas diferentes de la computadora tienen formas diferentes de almacenar información del texto en archivos. Un gran asunto es el caracter fin de línea o caracteres, pero los otros asuntos también pueden existir. Sin embargo, Java tiene un tipo de especial del flujo IO – el DataOutputStream – que promete eso, a pesar de qué máquina los datos vienen, el almacenamiento de esa información estará en una forma que puede ser correctamente recuperada por cualquier otra máquina usando a un DataInputStream. Es decir, Java maneja todos los detalles específicos en la plataforma, lo cual es una gran parte de la promesa de Java. Así la bandera -p almacena todo en un único archivo en un formato universal. Haces un download de este archivo y el programa Java de la Web, y cuando corres CodePackager en este archivo sin la bandera -p los archivos todos serán extraídos a lugares correctos en tu sistema. (Puedes especificar un subdirectorio alterno; De otra manera los subdirectorios solamente serán creados en el directorio actual.) Para asegurar que ningún resto específico de sistema de formatos, los objetos File son usados en todas partes como una ruta o un archivo es descrito. Además, hay una comprobación de cordura: Un archivo vacío es colocado en cada subdirectorio; El nombre de ese archivo indica cuántos archivos deberías encontrar en ese subdirectorio. Aquí está el código, lo cual será relatado detalladamente al final del listado: //: CodePackager.java // "Packs" and "unpacks" the code in "Thinking // in Java" for cross-platform distribution. /* Commented so CodePackager sees it and starts a new chapter directory, but so you don't have to worry about the directory where this program lives: package c17; */ import java.util.*; import java.io.*; class Pr { static void error(String e) { System.err.println("ERROR: " + e); System.exit(1); } } class IO { static BufferedReader disOpen(File f) { BufferedReader in = null; try { in = new BufferedReader( new FileReader(f)); } catch(IOException e) { Pr.error("could not open " + f); } return in; } static BufferedReader disOpen(String fname) { return disOpen(new File(fname)); } static DataOutputStream dosOpen(File f) { DataOutputStream in = null ; try { in = new DataOutputStream( new BufferedOutputStream( new FileOutputStream(f))); } catch(IOException e) { Pr.error("could not open " + f); } return in; } static DataOutputStream dosOpen(String fname) { return dosOpen(new File(fname)); } static PrintWriter psOpen(File f) { PrintWriter in = null ; try { in = new PrintWriter( new BufferedWriter( new FileWriter(f))); } catch(IOException e) { Pr.error("could not open " + f); } return in; } static PrintWriter psOpen(String fname) { return psOpen(new File(fname)); } static void close(Writer os) { try { os.close(); } catch(IOException e) { Pr.error("closing " + os); } } static void close(DataOutputStream os) { try { os.close(); } catch(IOException e) { Pr.error("closing " + os); } } static void close(Reader os) { try { os.close(); } catch(IOException e) { Pr.error("closing " + os); } } } class SourceCodeFile { public static final String startMarker = "//:", // Start of source file endMarker = "} ///:~" , // End of source endMarker2 = "}; ///:~", // C++ file end beginContinue = "} ///:Continued", endContinue = "///:Continuing", packMarker = "###", // Packed file header tag eol = // Line separator on current system System.getProperty( "line.separator"), filesep = // System's file path separator System.getProperty( "file.separator"); public static String copyright = ""; static { try { BufferedReader cr = new BufferedReader( new FileReader( "Copyright.txt" )); String crin; while((crin = cr.readLine()) != null) copyright += crin + "\n"; cr.close(); } catch(Exception e) { copyright = ""; } } private String filename, dirname, contents = new String(); private static String chapter = "c02"; // The file name separator from the old system: public static String oldsep; public String toString() { return dirname + filesep + filename; } // Constructor for parsing from document fi le: public SourceCodeFile(String firstLine, BufferedReader in) { dirname = chapter; // Skip past marker: filename = firstLine.substring( startMarker.length()).trim(); // Find space that terminates file name: if(filename.indexOf(' ') != -1) filename = filename.substring( 0, filename.indexOf(' ')); System.out.println("found: " + filename); contents = firstLine + eol; if(copyright.length() != 0) contents += copyright + eol; String s; boolean foundEndMarker = false; try { while((s = in.readLine()) != null) { if(s.startsWith(startMarker)) Pr.error("No end of file marker for " + filename); // For this program, no spaces before // the "package" keyword are allowed // in the input source code: else if(s.startsWith("package")) { // Extract package name: String pdir = s.substring( s.indexOf(' ')).trim(); pdir = pdir.substring( 0, pdir.indexOf(';')).trim(); // Capture the chapter from the package // ignoring the 'com' subdirectories: if(!pdir.startsWith( "com" )) { int firstDot = pdir.indexOf('.'); if(firstDot != -1) chapter = pdir.substring(0,firstDot); else chapter = pdir; } // Convert package name to path name: pdir = pdir.replace( '.', filesep.charAt(0)); System.out.println("package " + pdir); dirname = pdir; } contents += s + eol; // Move past continuations: if(s.startsWith(beginContinue)) while((s = in.readLine()) != null) if(s.startsWith(endContinue)) { contents += s + eol; break; } // Watch for end of code listing: if(s.startsWith(endMarker) || s.startsWith(endMarker2)) { foundEndMarker = true; break; } } if(!foundEndMarker) Pr.error( "End marker not found before EOF"); System.out.println( "Chapter: " + chapter); } catch(IOException e) { Pr.error("Error reading line" ); } } // For recovering from a packed file: public SourceCodeFile(BufferedReader pFile) { try { String s = pFile.readLine(); if(s == null) return; if(!s.startsWith(packMarker)) Pr.error("Can't find " + packMarker + " in " + s); s = s.substring( packMarker.length()).trim(); dirname = s.substring(0, s.indexOf('#')); filename = s.substring(s.indexOf('#') + 1); dirname = dirname.replace( oldsep.charAt(0), filesep.charAt(0)); filename = filename.replace( oldsep.charAt(0), filesep.charAt(0)); System.out.println( "listing: " + dirname + filesep + filename); while((s = pFile.readLine()) != null) { // Watch for end of code listing: if(s.startsWith(endMarker) || s.startsWith(endMarker2)) { contents += s; break; } contents += s + eol; } } catch(IOException e) { System.err.println( "Error reading line" ); } } public boolean hasFile() { return filename != null; } public String directory() { return dirname; } public String filename() { return filename; } public String contents() { return contents; } // To write to a packed file: public void writePacked(DataOutputStream out) { try { out.writeBytes( packMarker + dirname + "#" + filename + eol); out.writeBytes(contents); } catch(IOException e) { Pr.error("writing " + dirname + filesep + filename); } } // To generate the actual file: public void writeFile(String rootpath) { File path = new File(rootpath, dirname); path.mkdirs(); PrintWriter p = IO.psOpen(new File(path, filename)); p.print(contents); IO.close(p); } } class DirMap { private Hashtable t = new Hashtable(); private String rootpath; DirMap() { rootpath = System.getProperty("user.dir"); } DirMap(String alternateDir) { rootpath = alternateDir; } public void add(SourceCodeFile f){ String path = f.directory(); if(!t.containsKey(path)) t.put(path, new Vector()); ((Vector)t.get(path)).addElement(f); } public void writePackedFile(String fname) { DataOutputStream packed = IO.dosOpen(fname); try { packed.writeBytes("###Old Separator:" + SourceCodeFile.filesep + "###\n"); } catch(IOException e) { Pr.error("Writing separator to " + fname); } Enumeration e = t.keys(); while(e.hasMoreElements()) { String dir = (String)e.nextElement(); System.out.println( "Writing directory " + dir); Vector v = (Vector)t.get(dir); for(int i = 0; i < v.size(); i++) { SourceCodeFile f = (SourceCodeFile)v.elementAt(i); f.writePacked(packed); } } IO.close(packed); } // Write all the files in their directories: public void write() { Enumeration e = t.keys(); while(e.hasMoreElements()) { String dir = (String)e.nextElement(); Vector v = (Vector)t.get(dir); for(int i = 0; i < v.size(); i++) { SourceCodeFile f = (SourceCodeFile)v.elementAt(i); f.writeFile(rootpath); } // Add file indicating file quantity // written to this directory as a check: IO.close(IO.dosOpen( new File(new File(rootpath, dir), Integer.toString(v.size())+".files" ))); } } } public class CodePackager { private static final String usageString = "usage: java CodePackager packedFileName" + "\nExtracts source code files from packed \n" + "version of Tjava. doc sources into " + "directories off current directory\n" + "java CodePackager packedFileName newDir\n" + "Extracts into directories off newDir\n" + "java CodePackager -p source.txt packedFile" + "\nCreates packed version of source files" + "\nfrom text version of Tjava.doc"; private static void usage() { System.err.println(usageString); System.exit(1); } public static void main(String[] args) { if(args.length == 0) usage(); if(args[0].equals("-p")) { if(args.length != 3) usage(); createPackedFile(args); } else { if(args.length > 2) usage(); extractPackedFile(args); } } private static String currentLine; private static BufferedReader in; private static DirMap dm; private static void createPackedFile(String[] args) { dm = new DirMap(); in = IO.disOpen(args[1]); try { while((currentLine = in.readLine()) != null) { if(currentLine.startsWith( SourceCodeFile.startMar ker)) { dm.add(new SourceCodeFile( currentLine, in)); } else if(currentLine.startsWith( SourceCodeFile.endMarker)) Pr.error("file has no start marker" ); // Else ignore the input line } } catch(IOException e) { Pr.error("Error reading " + args[1]); } IO.close(in); dm.writePackedFile(args[2]); } private static void extractPackedFile(String[] args) { if(args.length == 2) // Alternate directory dm = new DirMap(args[1]); else // Current directory dm = new DirMap(); in = IO.disOpen(args[0]); String s = null; try { s = in.readLine(); } catch(IOException e) { Pr.error("Cannot read from " + in); } // Capture the separator used in the system // that packed the file: if(s.indexOf("###Old Separator:") != -1 ) { String oldsep = s.substring( "###Old Separator:".length()); oldsep = oldsep.substring( 0, oldsep. indexOf('#')); SourceCodeFile.oldsep = oldsep; } SourceCodeFile sf = new SourceCodeFile(in); while(sf.hasFile()) { dm.add(sf); sf = new SourceCodeFile(in); } dm.write(); } } ///:~ Primero notarás la declaración package que se dejó comentado. Ya que éste es el primer programa en el capítulo, la declaración package hay que decirle a CodePackager que el capítulo ha cambiado, pero meterla en un paquete sería un problema. Cuando creas un paquete, amarras el programa resultante a una estructura particular del directorio, lo cual es bueno para muchos de los ejemplos en este libro. Aquí, sin embargo, el programa CodePackager debe ser compilado y ejecutado desde un directorio arbitrario, así es que la declaración package se deja comentada. Todavía se parecerá a una declaración común package para CodePackager, sin embargo, ya que el programa no es lo suficientemente sofisticado para detectar comentarios multilínea. (No tiene necesidad para tal sofisticación, un hecho que viene bien aquí.) Las primeras dos clases son clases del soporte/utilidad diseñadas para hacer el resto de programa más consistente para escribir y más fácil para leer. La primera parte, Pr, es similar a la biblioteca ANSI C perror, ya que imprime un mensaje de error (pero sale del programa). La segunda categoría narra de forma resumida la creación de archivos, un proceso que fue demostrado en el Capítulo 12 como una que rápidamente se vuelve poco conciso y molesto. En el Capítulo 12, la solución propuesta creó clases nuevas, pero aquí las llamadas estáticas de método son usadas. Dentro de esos métodos las excepciones apropiadas son percibidas y distribuidas. Estos métodos le hacen al resto de código mucho más claro para leer. La primera clase que ayuda a solucionar el problema es SourceCodeFile , que represente toda la información (incluyendo el contenido, nombre de archivo, y directorio) para un archivo de código fuente en el libro. También contiene un conjunto de constantes String representando los marcadores que empiezan y finalizan un archivo, un marcador usado dentro el archivo empacado, el separador de fin de línea del sistema actual y el separador de la ruta del archivo (note el uso de System.getProperty() para obtener la versión local), y un aviso de derecho de autor, que es extraído del siguiente archivo Copyright.txt. ////////////////////////////////////////////////// // Copyright (c) Bruce Eckel, 1998 // Source code file from the book "Thinking in Java" // All rights reserved EXCEPT as allowed by the // following statements: You may freely use this file // for your own work (personal or commercial), // including modifications and distribution in // executable form only. Permission is granted to use // this file in classroom situations, including its // use in presentation materials, as long as the book // "Thinking in Java" is cited as the source. // Except in classroom situations, you may not copy // and distribute this code; instead, the sole // distribution point is http://www.BruceEckel.com // (and official mirror s ites) where it is // freely available. You may not remove this // copyright and notice. You may not distribute // modified versions of the source code in this // package. You may not use this file in printed // media without the express permission of the // author. Bruce Eckel makes no representation about // the suitability of this software for any purpose. // It is provided "as is" without express or implied // warranty of any kind, including any implied // warranty of merchantability, fitness for a // particular purpose or non-infringement. The entire // risk as to the quality and performance of the // software is with you. Bruce Eckel and the // publisher shall not be liable for any damages // suffered by you or any third party as a result of // using or distributing software. In no event will // Bruce Eckel or the publisher be liable for any // lost revenue, profit, or data, or for direct, // indirect, special, consequential, incidental, or // punitive damages, however caused and regardless of // the theory of liability, arising out of the use of // or inability to use software, even if Bruce Eckel // and the publisher have been advised of the // possibility of such damages. Should the software // prove defective, you assume the cost of all // necessary servicing, repair, or correction. If you // think you've found an error, please email all // modified files with clearly commented changes to: // [email protected]. (please use the same // address for non-code errors found in the book). ////////////////////////////////////////////////// Al extraer archivos de un archivo empacado, el separador del archivo del sistema que empacó el archivo es también notable, así es que puede ser reemplazado con el correcto para el sistema local. El nombre del subdirecto rio para el capítulo actual es guardado en el campo chapter, lo cual es inicializado a c02. (Notarás que el listado en el Capítulo 2 no contiene una declaración package.) La única vez que el campo de capítulo cambia es cuando una declaración package es descubierta en el archivo corriente. Construyendo un archivo empacado El primer constructor está acostumbrado a extraer un archivo de la versión del texto de ASCII de este libro. El código llamado (que aparece más abajo en el listado) lee cada línea hasta que encuentra uno que corresponde al comienzo de un listado. En ese momento, crea un objeto nuevo SourceCodeFile , pasándolos a la primera línea (que ya ha sido leídos por el código llamado) y el objeto BufferedReader del cual extrae el resto de listado de código fuente. En este punto, comienzas a ver el uso pesado de los métodos String . Para extraer el nombre de archivo, la versión sobrecargada de substring() es llamada que tomas el comienzo del desplazamiento y va al final del String. Este índice de comienzo se produce encontrando al length () del startMarker. trim() remueve espacios en blanco de ambos finales del String. La primera línea también puede tener palabras después del nombre del archivo; Estos son detectados usando indexOf(), lo cual retorna -1 si no pudiera encontrar el carácter que buscas y el valor donde la primera instancia de ese carácter es encontrada si lo hace. Note que allí está también una versión sobrecargada de indexOf() que toma a un String en lugar de un carácter. Una vez que el nombre de archivo es analizado gramaticalmente y almacenado, la primera línea es colocada dentro del contenido String (que se usa para contener el texto entero del listado de código fuente). En este punto, el resto de las líneas son leídas y concatenadas en el contenido String. Es no completamente tan simple, ya que en ciertas situaciones requieren un manejo especial. Un caso es la detección de errores: Si lo ejecutas en un startMarker, quiere decir que ningún marcador de fin estaba posado al final del listado que está actualmente siendo coleccionado. Ésta es una condición de error que aborta el programa. La segunda causa especial es la palabra clave package. Aunque Java es un lenguaje de forma independiente, este programa pide que la palabra clave package esté en el comienzo de la línea. Cuando la palabra clave package se ve, el nombre del paquete es extraído buscando el espacio al comienzo y el punto y coma al final. (Note que esto también pudo haber sido realizado en una operación única usando al substring() sobrecargado que toma ambos los índices del comienzo e índices finales.) Luego los puntos en el nombre del paquete son reemplazados por el separador del archivo, aunque una suposición está hecha aquí es que el separador del archivo es sólo un carácter largo. Esto es probablemente cierto en todos los sistemas, pero es un lugar para ver si hay problemas. El comportamiento predeterminado es concatenar cada línea para contenido, junto con el string fin de línea, hasta que el endMarker es descubierto, el cual señala que el constructor debería terminar. Si el fin del archivo es encontrado antes de que el endMarker se vea, ese es un error. Extrayendo desde un archivo empaquetado El segundo constructor se usa para recuperar los archivos de código fuente desde un archivo empaquetado. Aquí, el método llamado no tiene que preocuparse por saltar por encima del texto intermedio. El archivo contiene todos los archivos de código fuente, colocado extremo a extremo. Todo lo que necesitas dar para este constructor es el BufferedReader donde la información viene, y el constructor la toma de allí. Hay algo de meta información, sin embargo, al principio de cada listado, y esto es denotado por el packMarker. Si el packMarker no está allí, quiere decir que la persona que llama está equivocadamente tratando de usar a este constructor donde no es apropiado. Una vez que el packMarker es encontrado, es quitado y el nombre del directorio (terminado por un '#') y el nombre de archivo (que vaya al fin de la línea) son extraídos. En ambos casos, el carácter viejo del separador es reemplazado por uno que está reciente para esta máquina usando el método String replace (). El separador viejo está posado al principio del archivo empaquetado, y verás cómo es extraído este después en el listado. El resto del constructor es muy simple. Lee y concatena cada línea para los contents hasta que el endMarker sea encontrado. Acceder y escribir los listados El siguiente conjunto de métodos son asesores simples: directory(), filename() (nota el método puedes tener lo mismo ortografía y la capitalización como el campo) y contents (), y hasFile() para indicar si este objeto contiene un archivo o no. (La necesidad para esto se verá más adelante.) Los tres métodos finales están preocupados con escribir a este lista do de código en un archivo, ya sea un archivo empaquetado por writePacked() o un archivo fuente Java por writeFile (). Todo lo que writePacked() necesita es el DataOutputStream, lo cual fue abierto en otra parte, y representa el archivo que está siendo escrito. Pone la información del encabezado en la primera línea y luego llama a writeBytes() a escribir a contents en un formato “universal”. Al escribir el archivo fuente Java, el archivo debe ser creado. Esto se hace por IO.psOpen(), dándole un objeto File que contiene no sólo el nombre de archivo pero también la ruta. Pero la pregunta ahora es: ¿Existe esta ruta? El usuario tiene la opción de colocar todos los directorios de código de la fuente dentro de un subdirectorio completamente diferente, lo cual aun no podría existir. Así es que antes de que cada archivo sea escrito, File.mkdirs() es llamado con la ruta en la que quieres escribir el archivo. Esto hará la ruta entera al mismo tiempo. Conteniendo la colección entera de listados Conviene organizar los listados como subdirectorios mientras la colección entera se construye en la memoria. Una razón es otra comprobación de cordura: Como cada subdirectorio de listados es creado, un archivo adicional será añadido cuyo nombre contiene el número de archivos en ese directorio. La clase DirMap produce este efecto y demuestra el concepto de un “multimapa.” Esto es implementado usando a un Hashtable cuyas claves son los subdirectorios siendo creados y cuyos valores son objetos Vector conteniendo los objetos SourceCodeFile en ese directorio particular. Así, en lugar de asociar una clave para un valor único, el “multimapa” asocia una clave para un conjunto de valores por el Vector asociado. Aunque esto suena complejo, es notablemente franco implementar. Verás que la mayor parte del tamaño de la clase DirMap es debido a las porciones que escriben a archivos, no para la implementación “multimapa”. Hay dos formas que puedes hacer un DirMap: El constructor predeterminado asume que quieres que los directorios se ramifiquen completamente de lo actual, y el segundo constructor te deja especificar una ruta absoluta alterna para el directorio que empieza. El método add() es donde un montón de acción densa ocurre. Primero, el directory () es extraído del SourceCodeFile que quieres agregar, y luego el Hashtable es examinado para ver si ya contiene esa clave. De lo contrario, un Vector nuevo es añadido al Hashtable y asociado con esa clave. En este punto, el Vector está allí, de una u otra manera, y es extraído así es que el SourceCodeFile puede agregarse. Porque los Vectors pueden estar fácilmente combinados con Hashtables como éste, el poder de ambos es amplificado. Escribir un archivo empaquetado involucra a abrir el archivo para escribir (como un DataOutputStream así los datos son universalmente recuperables) y escribiendo la información del encabezado acerca del separador viejo en la primera línea. Después, un Enumeration de las claves Hashtable es producido y dado un paso hasta el final para seleccionar cada directorio e irle a traer al Vector asociado con ese directorio así cada SourceCodeFile en que a Vector le pueden ser escritos para el archivo empaquetado. Escribiendo los archivos de origen Java a sus directorios en write() son casi idénticos para writePackedFile () desde que ambos métodos simplemente llamen el método apropiado en SourceCodeFile. Aquí, sin embargo, la ruta raíz es pasada en SourceCodeFile.writeFile() y cuando todos los archivos han sido escritos el archivo adicional con el nombre conteniendo el número de archivos es también escrito. El programa principal Las clases descritas anteriormente son usadas dentro de CodePackager. Primero ves el uso string que queda impreso cada vez que el usuario final invoca el programa incorrectamente, junto con el método usage() que lo llama y sale del programa. Todo lo que main() hace es determinar si quieres crear un extracto o archivo empaquetado de uno, luego asegura que los argumentos son correctos y llama el método apropiado. Cuando un archivo empaquetado es creado, - se asume - se hace en el directorio actual, así es que el DirMap es creado usando al constructor predeterminado. Después de que el archivo es abierto cada línea es leída y examinada para condiciones particulares: 1. Si la línea comienza con el marcador que empieza para un listado de código fuente, un objeto nuevo SourceCodeFile es creado. El constructor lee en el resto de listado de programa. El manipulador que resulte es directamente añadida al DirMap. 2. Si la línea comienza con el marcador de fin para un listado de código fuente, algo ha salido mal, ya que los marcadores de fin deberían ser encontrados sólo por el constructor SourceCodeFile. Al extraer un archivo empaquetado, la extracción puede estar en el directorio actual o en un directorio alterno, así es que el objeto DirMap es creado consecuentemente. El archivo es abierto y la primera línea es leída. El separador viejo de información de la ruta del archivo es extraído de esta línea. Luego la entrada se usa para crear el primer objeto SourceCodeFile , el cual es añadido al DirMap. Los objetos nuevos SourceCodeFile son creados y añadidos con tal de que contengan un archivo. (Lo último creado simplemente retornará cuando se queda sin entrada y luego hasFile() retornará false.) Comprobando estilo de capitalización Aunque el ejemplo previo puede venir bien como un guía para algún proyecto tuyo que involucra proceso de texto, este proyecto será directamente útil porque realiza una comprobación de estilo para asegurarse de que tu capitalización se conforma al estilo Java efectiva. Abre cada archivo .java en el directorio actual y extrae todos los nombres de clase e identificadores, luego te muestra si cualquiera de ellos no encuentra el estilo Java. Para que el programa maneje correctamente, primero debes fortalecer un depositario de nombre de clase para contener todos los nombres de clase en la biblioteca estándar Java. Haces esto moviéndose hacia todos los subdirectorios de código fuente para la biblioteca estándar Java y corriendo a ClassScanner en cada subdirectorio. Provee como los argumentos el nombre del archivo del depositario (usando la misma ruta y nombre cada vez) y la opción de línea de comando -a señala que los nombres de clase deberían ser añadidos al depositario. Para usar el programa para comprobar tu código, lo corres y le das la ruta y nombre del depositario a usar. Comprobará todas las clases e identificadores en el directorio actual y te dirá cuáles no siguen el estilo típico de capitalización Java. Deberías ser consciente de que el programa no sea perfecto; Allí pocas veces señalará cuando se piensa que hay un problema pero al mirar el código verás que nada necesita variarse. Esto es un poco molesto, pero es todavía mucho más fácil tratando de encontrar todos estos casos mirando tu código. La explicación inmediatamente sigue el listado: //: ClassScanner.java // Scans all files in directory for classes // and identifiers, to check capitalization. // Assumes properly compiling code listings. // Doesn't do everything right, but is a very // useful aid. import java.io.*; import java.util.*; class MultiStringMap extends Hashtable { public void add(String key, String value) { if(!containsKey(key)) put(key, new Vector()); ((Vector)get(key)).addElement(value); } public Vector getVector(String key) { if(!containsKey(key)) { System.err.println( "ERROR: can't find key: " + key); System.exit(1); } return (Vector)get(key); } public void printValues(PrintStream p) { Enumeration k = keys(); while(k.hasMoreE lements()) { String oneKey = (String)k.nextElement(); Vector val = getVector(oneKey); for(int i = 0; i < val.size(); i++) p.println((String)val.elementAt(i)); } } } public class ClassScanner { private File path; private String[] fileList; private Properties classes = new Properties(); private MultiStringMap classMap = new MultiStringMap(), identMap = new MultiStringMap(); private StreamTokenizer in; public ClassScanner() { path = new File( "."); fileList = path.list( new JavaFilter()); for(int i = 0; i < fileList.length; i++) { System.out.println(fileList[i]); scanListing(fileList[i]); } } void scanListing(String fname) { try { in = new StreamTokenizer( new BufferedReader( new FileReader(fname))); // Doesn't seem to work: // in.slashStarComments(true); // in.slashSlashComments(true); in.ordinaryChar('/'); in.ordinaryChar('.'); in.wordChars('_', '_'); in.eolIsSignificant(true ); while(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') eatComments(); else if(in.ttype == StreamTokenizer.TT_WORD) { if(in.sval.equals("class" ) || in.sval.equals("interface")) { // Get class name: while (in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_WORD) ; classes.put(in.sval, in.sval); classMap.add(fname, in.sval); } if(in.sval.equals("import") || in.sval.equals("package")) discardLine(); else // It's an identifier or keyword identMap.add(fname, in.sval); } } } catch(IOException e) { e.printStackTrace(); } } void discardLine() { try { while(in.nextToken() != StreamTokenizer.TT_EOF && in.tt ype != StreamTokenizer.TT_EOL) ; // Throw away tokens to end of line } catch(IOException e) { e.printStackTrace(); } } // StreamTokenizer's comment removal seemed // to be broken. This extracts them: void eatComments() { try { if(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') discardLine(); else if(in.ttype != '*') in.pushBack(); else while(true ) { if(in.nextToken() == StreamTokenizer.TT_EOF) break; if(in.ttype == '*') if(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype == '/') break; } } } catch(IOException e) { e.printStackTrace(); } } public String[] classNames() { String[] result = new String[classes.size()]; Enumeration e = classes.keys(); int i = 0; while(e.hasMoreElements()) result[i++] = (String)e.nextElement(); return result; } public void checkClassNames() { Enumeration files = classMap.keys(); while(files.hasMoreElements()) { String file = (String)files.nextElement(); Vector cls = classMap.getVector(file); for(int i = 0; i < cls.size(); i++) { String className = (String)cls.elementAt(i); if(Character.isLowerCase( className.charAt(0))) System.out.println( "class capitalization error, file: " + file + ", class: " + className); } } } public void checkIdentNames() { Enumeration files = identMap.keys(); Vector reportSet = new Vector(); while(files.hasMoreElements()) { String file = (String)files.nextElement(); Vector ids = identMap.getVector(file); for(int i = 0; i < ids.size(); i++) { String id = (String)ids.elementAt(i); if(!classes.contains(id)) { // Ignore identifiers of length 3 or // longer that are all uppercase // (probably static final values): if(id.length() >= 3 && id.equals( id.toUpperCase())) continue ; // Check to see if first char is upper: if(Character.isUpperCase(id.charAt(0))){ if(reportSet.indexOf(file + id) == -1){ // Not reported yet reportSet.addElement(file + id); System.out.println( "Ident capitalization error in:" + file + ", ident: " + id); } } } } } } static final String usage = "Usage: \n" + "ClassScanner classnames -a\n" + "\tAdds all the class names in this \n" + "\tdirectory to the repository file \n" + "\tcalled 'classnames'\n" + "ClassScanner classnames\n" + "\tChecks all the java files in this \n" + "\tdirectory for capitalization errors, \n" + "\tusing the repository file 'classnames'"; private static void usage() { System.err.println(usage); System.exit(1); } public static void main(String[] args) { if(args.length < 1 || args.length > 2) usage(); ClassScanner c = new ClassScanner(); File old = new File(args[0]); if(old.exists()) { try { // Try to open an existing // properties file: InputStream oldlist = new BufferedInputStream( new FileInputStream(old)); c.classes.load(oldlist); oldlist.close(); } catch(IOException e) { System.err.println("Could not open " + old + " for reading"); System.exit(1); } } if(args.length == 1) { c.checkClassNames(); } c.checkIdentNames(); } // Write the class names to a repository: if(args.length == 2) { if(!args[1].equals( "-a")) usage(); try { BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(args[0])); c.classes.save(out, "Classes found by ClassScanner.java"); out.close(); } catch(IOException e) { System.err.println( "Could not write " + args[0]); System.exit(1); } } } class JavaFilter implements FilenameFilter { public boolean accept(File dir, String name) { // Strip path information: String f = new File(name).getName(); return f.trim().endsWith(".java"); } } ///:~ La clase MultiStringMap es una herramienta que te permite asociar un grupo de string encima de cada entrada crucial. Como en el ejemplo previo, usa a un Hashtable (esta vez con herencia) con la clave como el sencillo string que se asoció encima del valor Vector. El método add() simplemente inspecciona para ver si hay ya una clave en el Hashtable , y en caso de que no coloca uno allí. El método getVector() produce a un Vector para una clave particular, y printValues(), lo cual es muy apropiado para corrección de errores, imprime todo el valor Vector por Vector. Para conservar la vigencia básica, los nombres de clase de las bibliotecas estándar Java son todo puestos en un objeto Properties (de la biblioteca estándar Java). Recuerde que un objeto Properties es un Hashtable que contiene sólo objetos String para ambos la clave y entradas de valor. Sin embargo, puede ser salvado a disco y recuperado de disco en una llamada de método, así es que es ideal para el depositario de nombres. Realmente, necesitamos sólo una lista de nombres, y un Hashtable no puede aceptar nulo para ya sea su clave o su entrada de valor. Así es que el mismo objeto servirá para ambos la clave y el valor. Para las clases e identificadores que son descubiertos para los archivos en un directorio particular, dos MultiStringMaps son usados: ClassMap e identMap . También, cuando el programa empieza le carga el depositario estándar de nombre de clase en el objeto Properties llamadas classes, y cuando un nombre nuevo de clase es encontrado en el directorio local que es también añadido a las clases así como también para classMap. Así, classMap puede estar acostumbrado a dar un paso hasta el final todas las clases en el directorio local, y las clases pueden usarse para ver si la muestra actual es un nombre de clase (que indica que una definición de un objeto o método empieza, así es que agarra las siguientes muestras – hasta un punto y coma – y las introduce en identMap). El constructor predeterminado para ClassScanner crea una lista de nombres de archivo (usando la implementación JavaFilter de FilenameFilter, como se ha descrito en el Capítulo 12). Luego llama a scanListing() para cada nombre de archivo. Dentro de scanListing() el archivo del código fuente es abierto y revuelto en un StreamTokenizer. En la documentación, pasando true a slashStarComments () y slashSlashComments() - se supone - devasta esos comentarios fuera, pero esto parece ser un poco defectuoso (realmente no trabaja en Java 1.0). En lugar de eso, esas líneas son comentadas y los comentarios son extraídos por otro método. Para hacer esto, el debe ser capturado como un carácter común en vez de dejar a StreamTokenizer absorberlo como parte de un comentario, y el método ordinaryChar() dice al StreamTokenizer que haga esto. Esto es también cierto por puntos ('.'), Ya que queremos tener las llamadas de método jaladas aparte en identificadores individuales. Sin embargo, la línea subrayada, el cual es ordinariamente tratado por StreamTokenizer como un carácter individual, debería quedar como parte de identificadores desde que aparezca en tales valores finales estáticos como TT_EOF etcétera., Usado en este mismo programa. El método wordChars () toma un rango de personajes que quieres añadirle a esos que se quedan dentro de una muestra que está siendo analizada gramaticalmente como una palabra. Finalmente, al analizar gramaticalmente por comentarios de un línea o descartar una línea necesitamos conocer cuándo ocurre un fin de línea, así llamando a eolIsSignificant(true) el eol aparecerá en vez de siendo absorbido por el StreamTokenizer. El resto de scanListing() lee y reacciona a los valores simbólicos hasta que el final del archivo, significado cuando nextToken() devuelva el valor estático final StreamTokenizer.TT_EOF. Si la muestra es uno que es potencialmente un comentario, así eatComments() son llamados para ocuparse de ella. La única otra situació n que estamos interesados aquí dentro es que si es una palabra, del cual hay algunas causas especiales. Si la palabra es clase o interfaz entonces la siguiente muestra representa una clase o un nombre de la interfaz, y es introducida en classs y classMap. Si la palabra es import o package, entonces no queremos el resto de la línea. Cualquier otra cosa debe ser un identificador o una palabra clave (en el cual estamos interesados) (los cuales no son, pero son toda letra minúscula de cualquier manera así es que no echará a perder cosas para echar esos). Estos son añadidos a identMap. El método discardLine () es una herramienta simple que busca el fin de una línea. Nota que siempre que obtienes una muestra nueva, debes revisar en busca del fin del archivo. El método eatComments() es llamado cada vez que un slash adelantado es encontrado en el bucle principal de análisis gramatical. Sin embargo, eso necesariamente no quiere decir que un comentario haya sido encontrado, así es que la siguiente muestra debe ser extra ída para ver si es otro slash adelantado (en cuyo caso la línea es descartada) o un asterisco. ¡Pero si son ningún de esos, quiere decir que la muestra que justamente has arrancado es necesaria retornarlo en el bucle general de análisis sintáctico! Afortunadamente, el método pushBack () te permite retroceder la muestra actual encima del flujo de entrada a fin de que cuando el bucle principal de análisis gramatical llama a nextToken() traerá el mismo que justamente retrocediste. Por conveniencia, el método classNames() produce un arreglo de todos los nombres en la colección de clases. Este método no es usado en el programa pero es de ayuda para depurar. Los siguientes dos métodos son los únicos en los cuales la comprobación real tiene lugar. En checkClassNames(), los nombres de clase son extraídos del classMap (el cual, recuerda que, contiene sólo los nombres en este directorio, organizado por el nombre de archivo así es que el nombre de archivo puede ser impreso junto con el nombre errante de clase). Esto está consumado atrayendo a cada Vector asociado y dando un paso a través de eso, viendo si el primer carácter es letra minúscula. Si es así, el mensaje apropiado de error es impreso. En checkIdentNames(), un acercamiento similar es tomado: Cada nombre de identificador es extraído de identMap. Si el nombre no está en la lista de clases, - se asume - es un identificador o palabra clave. Una causa especial es comprobada: Si la longitud de identificador es 3 o más y todos los caracteres son mayúsculas, este identificador es ignorado porque es probablemente un valor final estático como TT_EOF . Por supuesto, éste no es un algoritmo perfecto, pero da por supuesto que eventualmente notarás cualquiera de todos los identificadores mayúsculas que están fuera de lugar. En lugar de reportar cada identificador que comienza con un carácter mayúscula, este método le sigue la pista a los cuales ya han sido reportados en un Vector llamado reportSet(). Esto trata al Vector como un conjunto que te dice ya sea que un artículo está ya en el conjunto. El artículo se produce concatenando el nombre de archivo e identificador. Si el elemento no está en el conjunto, se agrega y entonces el informe se hace. El resto de listado está implícito de main(), lo cual se ocupa manejando los argumentos de la línea de comando e imaginándose que fuera de si estás fortaleciendo a un depositario de nombres de clase de la biblioteca estándar Java o comprobando la validez de código que has escrito. En ambos casos hace un objeto ClassScanner. Si estás construyendo a un depositario o usando uno, debes tratar de abrir al depositario existente. Haciendo un objeto File y haciendo pruebas para la existencia, puedes decidirte ya sea abrir el archivo y load() las clases de la lista de Properties dentro de ClassScanner. (Las clases del depositario se añaden a, en vez de sobrescribir, las clases encontradas por el constructor ClassScanner.) Si provees sólo un argumento de línea de comando quiere decir que quieres realizar una comprobación de los nombres de clase y los nombres de identificador, pero si provees dos argumentos (el segundo siendo a) construyes un depositario de nombre de clase. En este caso, un archivo de salida es abierto y el método Properties.save() se usa para escribir la lista en un archivo, junto co n uno string que provee información del archivo del encabezado. Una herramienta de búsqueda de método El capítulo 10 introdujo a la Java 1.1 concepto de reflexión y se usó esa característica para buscar métodos para una clase particular – ya sea la lista entera de métodos o un subconjunto de esos cuyos nombres corresponden a una palabra clave que provee. La magia de esto es que automáticamente os puede demostrar a todos ustedes los métodos para una clase sin obligarte a caminar arriba de la jerarquía de a l herencia examinando las clases base en cada nivel. Así, provee una herramienta ahorradora de tiempo valiosa para programar: Porque los nombres de la mayoría de nombres de método Java son hechos amablemente verboso y descriptivo, puedes ir en busca de los nombres de método que contienen una palabra particular de interés. Cuando encuentras lo que piensas que estás buscando, compruebas la documentación en línea. Sin embargo, por el Capítulo 10 no habías visto Swing, a fin de que la herramienta fuera desarrollada como una aplicación de línea de comando. Aquí está la versión GUI más útil, lo cual dinámicamente actualiza la salida cuando escribes y también te permite cortar y pegar desde la salida: //: DisplayMethods.java // Display the methods of any class inside // a window. Dynamically narrows your search. import java.awt.*; import java.awt.event.*; import java.applet.*; import java.lang.reflect.*; import java.io.*; public class DisplayMethods extends Applet { Class cl; Method[] m; Constructor[] ctor; String[] n = new String[0]; TextField name = new TextField(40), searchFor = new TextField(30); Checkbox strip = new Checkbox("Strip Qualifiers" ); TextArea results = new TextArea(40, 65); public void init() { strip.setState(true); name.addTextListener( new NameL()); searchFor.addTextListener( new SearchForL()); strip.addItemListener(new StripL()); Panel top = new Panel(), lower = new Panel(), p = new Panel(); top.add(new Label("Qualified class name:" )); top.add(name); lower.add( new Label("String to search for:")); lower.add(searchFor); lower.add(strip); p.setLayout(new BorderLayout()); p.add(top, BorderLayout.NORTH); p.add(lower, BorderLayout.SOUTH); setLayout(new BorderLayout()); add(p, BorderLayout.NORTH); add(results, BorderLayout.CENTER); } class NameL implements TextListener { public void textValueChanged(TextEvent e) { String nm = name.getText().trim(); if(nm.length() == 0) { results.setText("No match"); n = new String[0]; return; } try { cl = Class.forName(nm); } catch (ClassNotFoundException ex) { results.setText("No match"); return; } m = cl.getMethods(); ctor = cl.getConstructors(); // Convert to an array of Strings: n = new String[m.length + ctor.length]; for(int i = 0; i < m.length; i++) n[i] = m[i].toString(); for(int i = 0; i < ctor.length; i++) n[i + m.length] = ctor[i].toString(); reDisplay(); } } void reDisplay() { // Create the result set: String[] rs = new String[n.length]; String find = searchFor.getText(); int j = 0; // Select from the list if find exists: for (int i = 0; i < n.length; i++) { if(find == null) rs[j++] = n[i]; else if(n[i].indexOf(find) != -1) rs[j++] = n[i]; } results.setText( ""); if(strip.getState() == true) for (int i = 0; i < j; i++) results.append( StripQualifiers.strip(rs[i]) + "\n"); else // Leave qualifiers on for (int i = 0; i < j; i++) results.append(rs[i] + "\n"); } class StripL implements ItemListener { public void itemStateChanged(ItemEve nt e) { reDisplay(); } } class SearchForL implements TextListener { public void textValueChanged(TextEvent e) { reDisplay(); } } public static void main(String[] args) { DisplayMethods applet = new DisplayMethods(); Frame aFrame = new Frame("Display Methods"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(500,750); applet.init(); applet.start(); aFrame.setVisible(true); } } class StripQualifiers { private StreamTokenizer st; public StripQualifiers(String qualified) { st = new StreamTokenizer( new StringReader(qualified)); st.ordinaryChar(' '); } public String getNext() { String s = null; try { if(st.nextToken() != StreamTokenizer.TT_EOF) { switch(st.ttype) { case StreamTokenizer.TT_EOL: s = null ; break; case StreamTokenizer.TT_NUMBER: s = Double.toString(st.nval); break; case StreamTokenizer.TT_WORD: s = new String(st.sval); break; default: // single character in ttype s = String.valueOf((char)st.ttype); } } } catch(IOException e) { System.out.println(e); } return s; } public static String strip(String qualified) { StripQualifiers sq = new StripQualifiers(qualified); String s = "", si; while((si = sq.getNext()) != null) { int lastDot = si.lastIndexOf('.'); if(lastDot != -1) si = si.substring(lastDot + 1); s += si; } return s; } } ///:~ Algunas cosas que has visto antes. Al igual que con muchos de los programas GUI en este libro, esto es creado para realizar ambos como una aplicación y como un applet. También, la clase StripQualifiers es exactamente igual como estaba en el Capítulo 10. El GUI contiene un nombre TextField en el cual puedes introducir el nombre de clase con creces calificado que quieres buscar, y otro, searchFor, en el cual puedes entrar en el texto optativo para ir en busca dentro de la lista de métodos. El Checkbox te permite decir si quieres usar los nombres completamente calificados en la salida o si quieres la capacitación quitada. Finalmente, los resultados son desplegados en un TextArea. Notarás que no hay botones u otros componentes por los cuales señala que quieres que la búsqueda empiece. Eso es porque ambos de los TextFields y el Checkbox son monitoreados por sus objetos del oyente. Cada vez que haces un cambio, la lista está inmediatamente actualizada. Si cambias el texto dentro del campo name, el texto nuevo es capturado en la clase NameL . Si el texto no está vacío, es usado dentro de Class.forName() para tratar de buscar la clase. Como escribe, claro está, el nombre será incompleto y Class.forName() fallará, lo cual quiere decir que lanza una excepción. Esto es atrapado y la TextArea está lista para "No match". Pero tan pronto como introduces un nombre correcto (la capitalización tiene importancia), Class.forName() tiene éxito y getMethods() y getConstructors() devolverán arreglos de objetos Method y Constructor, respectivamente. Cada uno de lo s objetos en estos arreglos son convertidos en un String por toString () (éste produce la firma de método completo o del constructor) y ambas listas están combinados en n, un sencillo arreglo String. El arreglo n forma parte de la clase DisplayMethods y es usada en actualizar el despliegue cada vez que reDisplay() es llamado. Si cambias al Checkbox o componentes searchFor, sus oyentes simplemente llaman reDisplay(). reDisplay() crea un arreglo temporal String llamados rs (para el "conjunto de resultado"). El conjunto de resultado es cualquier copiado directamente de n si no esta la palabra find , o condicionalmente copiado de los Strings en n que contiene la palabra find. Finalmente, el strip Checkbox es interrogado para ver si el usuario quiere que los nombre s fueran devastados (el defecto es yes). Si es así, StripQualifiers .strip() hace el trabajo; Si no, la lista es simplemente desplegada. En init(), podrías pensar que abunda el trabajo ocupado involucrado en establecer el diseño. De hecho, cabe diseñar los componentes con menos trabajo, pero la ventaja de usar a BorderLayouts así es que le permite al usuario ajustar el tamaño de la ventana y hacer – en particular – al TextArea más grande, que quiere decir que puedes ajustar el tamaño para permitirte ver nombres más largos sin desplazar. Podrías encontrarte con que conservarás esta herramienta corriendo mientras programas, ya que provee una de las mejores “primeras líneas de ataque” cuando estás tratando de resolver qué método llamar. La teoría de complejidad Este programa fue modificado de código originalmente creado por Larry O'Brien, y se basa en los programas “Boids” creado por Craig Reynolds en 1986 para demostrar un aspecto de teoría de complejidad llamada “emergencia.” La meta aquí es producir una reproducción razonablemente parecida a la realidad de viajar en tropel o reuniendo en manada el comportamiento en animales estableciendo un conjunto pequeño de reglas simples para cada animal. Cada animal puede mirar la escena entera y todos los demás animales en la escena, pero reacciona sólo a un conjunto cercanos “flockmates.” El animal se mueve según tres comportamientos simples de la dirección: 1. Separación: Evita atestar flockmates locales. 2. Alineación: Sigue el encabezamiento común de flockmates locales. 3. Cohesión: Se Mueve hacia el centro del grupo de flockmates locales. Los modelos más elaborados pueden incluir obstáculos y la habilidad para los animales a predecir colisiones y evitarles a ellos, así es que los animales pueden fluir alrededor de objeto s fijos en el ambiente. Además, los animales también podrían recibir una meta, lo cual puede causar que el rebaño siga un camino deseado. Para la simplicidad, la evitación de obstáculo y el buscar metas no son incluidos en el modelo presentado aquí. La emergencia quiere decir que, a pesar de la naturaleza limitada de computadoras y la simplicidad de las reglas de la dirección, el resultado parece realista. Es decir, el comportamiento notablemente parecido a la realidad “emerge” de este modelo simple. El pro grama se replantea como una aplicación/applet combinada: //: FieldOBeasts.java // Demonstration of complexity theory; simulates // herding behavior in animals. Adapted from // a program by Larry O'Brien [email protected] import java.awt.*; import java.awt.event.*; import java.applet.*; import java.util.*; class Beast { int x, y, // Screen position currentSpeed; // Pixels per second float currentDirection; // Radians Color color; // Fill color FieldOBeasts field; // Where the Beast roams static final int GSIZE = 10; // Graphic size public Beast(FieldOBeasts f, int x, int y, float cD, int cS, Color c) { field = f; this.x = x; this.y = y; currentDirection = cD; currentSpeed = cS; color = c; } public void step() { // You move based on those within your sight: Vector seen = field.beastListInSector(this); // If you're not out in front if(seen.size() > 0) { // Gather data on those you see int totalSpeed = 0; float totalBearing = 0.0f; float distanceToNearest = 100000.0f; Beast nearestBeast = (Beast)seen.elementAt(0); Enumeration e = seen.elements(); while(e.hasMoreElements()) { Beast aBeast = (Beast) e.nextElement(); totalSpeed += aBeast.currentSpeed; float bearing = aBeast.bearingFromPointAlongAxis( x, y, currentDirection); totalBearing += bearing; float distanceToBeast = aBeast.distanceFromPoint(x, y); if(distanceToBeast < distanceToNearest) { nearestBeast = aBeast; distanceToNearest = distanceToBeast; } } // Rule 1: Match average speed of those // in the list: currentSpeed = totalSpeed / seen.size(); // Rule 2: Move towards the perceived // center of gravity of the herd: currentDirection = totalBearing / seen.size(); // Rule 3: Maintain a minimum distance // from those around you: if(distanceToNearest <= field.minimumDistance) { currentDirection = nearestBeast.currentDirection; currentSpeed = nearestBeast.currentSpeed; if(currentSpeed > field.maxSpeed) { currentSpeed = field.maxSpeed; } } } else { // You are in front, so slow down currentSpeed = (int)(currentSpeed * field.decayRate); } // Make the beast move: x += (int)(Math.cos(currentDirection) * currentSpeed); y += (int)(Math.sin(currentDirection) * currentSpeed); x %= field.xExtent; y %= field.yExtent; if(x < 0) x += field.xExtent; if(y < 0) y += field.yExtent; } public float bearingFromPointAlongAxis ( int originX, int originY, float axis) { // Returns bearing angle of the current Beast // in the world coordiante system try { double bearingInRadians = Math.atan( (this.y - originY) / (this.x - originX)); // Inverse tan has two solutions, so you // have to correct for other quarters: if(x < originX) { if(y < originY) { bearingInRadians += - (float)Math.PI; } else { bearingInRadians = (float)Math.PI - bearingInRadians; } } // Just subtract the axis (in radians): return (float) (axis - bearingInRadians); } catch(ArithmeticException aE) { // Divide by 0 error possible on this if(x > originX) { return 0; } else return (float) Math.PI; } } public float distanceFromPoint(int x1, int y1){ return (float) Math.sqrt( Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)); } public Point position() { return new Point(x, y); } // Beasts know how to draw themselves: public void draw(Graphics g) { g.setColor(color); int directionInDegrees = ( int)( (currentDirection * 360) / (2 * Math.PI)); int startAngle = directionInDegrees FieldOBeasts.halfFieldOfView; int endAngle = 90; g.fillArc(x, y, GSIZE, GSIZE, startAngle, endAngle); } } public class FieldOBeasts extends Applet implements Runnable { private Vector beasts; static float fieldOfView = (float) (Math. PI / 4), // In radians // Deceleration % per second: decayRate = 1.0f, minimumDistance = 10f; // In pixels static int halfFieldOfView = (int)( (fieldOfView * 360) / (2 * Math.PI)), xExtent = 0, yExtent = 0, numBeasts = 50, maxSpeed = 20; // Pixels/second boolean uniqueColors = true; Thread thisThread; int delay = 25; public void init() { if (xExtent == 0 && yExtent == 0) { xExtent = Integer.parseInt( getParameter("xExtent" )); yExtent = Integer.parseInt( getParameter("yExtent" )); } beasts = makeBeastVector(numBeasts, uniqueColors); // Now start the beasts a-rovin': thisThread = new Thread(this); thisThread.start(); } public void run() { while(true) { for(int i = 0; i < beasts.size(); i++){ Beast b = (Beast) beasts.elementAt(i); b.step(); } try { thisThread.sleep(delay); } catch(InterruptedException ex){} repaint(); // Otherwise it won't update } } Vector makeBeastVector( int quantity, boolean uniqueColors) { Vector newBeasts = new Vector(); Random generator = new Random(); // Used only if uniqueColors is on: double cubeRootOfBeastNumber = Math.pow((double)nu mBeasts, 1.0 / 3.0); float colorCubeStepSize = (float) (1.0 / cubeRootOfBeastNumber); float r = 0.0f; float g = 0.0f; float b = 0.0f; for(int i = 0; i < quantity; i++) { int x = (int) (generator.nextFloat() * xExtent); if(x > xExtent - Beast.GSIZE) x -= Beast.GSIZE; int y = (int) (generator.nextFloat() * yExtent); if(y > yExtent - Beast.GSIZE) y -= Beast.GSIZE; float direction = ( float )( generator.nextFloat() * 2 * Math.PI); int speed = (int)( generator.nextFloat() * (float)maxSpeed); if(uniqueColors) { r += colorCubeStepSize; if(r > 1.0) { r -= 1.0f; g += colorCubeStepSize; if( g > 1.0) { g -= 1.0f; b += colorCubeStepSize; if(b > 1.0) b -= 1.0f; } } } newBeasts.addElement( new Beast(this, x, y, direction, speed, new Color(r,g,b))); } return newBeasts; } public Vector beastListInSector(Beast viewer) { Vector output = new Vector(); Enumeration e = beasts.elements(); Beast aBeast = (Beast)beasts.elementAt(0); int counter = 0; while(e.hasMoreElements()) { aBeast = (Beast) e.nextElement(); if(aBeast != viewer) { Point p = aBeast.position(); Point v = viewer.position(); float bearing = aBeast.bearingFromPointAlongAxis( v.x, v.y, viewer.currentDirection); if(Math.abs(bearing) < fieldOfView / 2) output.addElement(aBeast); } } return output; } public void paint(Graphics g) { Enumeration e = beasts.elements(); while(e.hasMoreElements()) { ((Beast)e.nextElement()).draw(g); } } public static void main(String[] args) { FieldOBeasts field = new FieldOBeasts(); field.xExtent = 640; field.yExtent = 480; Frame frame = new Frame("Field 'O Beasts" ); // Optionally use a command-line argument // for the sleep time: if(args.length >= 1) field.delay = Integer.parseInt(args[0]); frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); frame.add(field, BorderLayout.CENTER); frame.setSize(640,480); field.init(); field.start(); frame.setVisible(true ); } } ///:~ Aunque ésta no es una reproducción perfecta del comportamiento en “Boids” de Craig Reynold el ejemplo, exhibe sus propias características fascinantes, el cual puedes modificar ajustando los números. Puedes encontrar más acerca del modelado de comportamiento de viajar en tropel y puedes ver una versión espectacular 3-D de Boids en la página de Craig Reynold http://www.hmt.com/cwr/boids.html. Para correr éste programa como un applet, pones la siguiente etiqueta de applet en un archivo de HTML: <applet code=FieldOBeasts width=640 height=480> <param name=xExtent value = "640"> <param name=yExtent value = "480"> </applet> Resumen Este capítulo demuestra que una cierta cantidad de las cosas más sofisticadas se pueden hacer con Java. También hace el punto que mientras Java ciertamente debe tener sus límites, esos límites son primordialmente relegados para el desempeño. (Cuando a los programas de procesos de texto les fue escrito, por ejemplo, las versiones C++ fueron mucho más rápidas – éste podría deberse en parte a una implementación ineficiente de la biblioteca IO, lo cual debería cambiar con el tiempo.) Los límites de Java no parecen estar en el área de expresividad. No sólo lo hace parece posible expresar justamente acerca de todo lo que puedes imaginar, pero Java parece orientada a facilitar esa expresión de escribir y leer. Por eso no entras corriendo a la pared de complejidad que a menudo cursa con lenguajes que son más trivial por usar que Java (al menos parecen de ese modo, al principio). Y con la librería JFC/Swing 1.2 de Java, emparejan la expresividad y la facilidad de uso del AWT mejora dramáticamente. Ejercicios 1. (Avanzado) Reescribe FieldOBeasts.java a fin de que su estado puede ser persistente. Implementa botones para permitirte salvar y recordar archivos diferentes del estado y continuar corriéndolos donde salió completamente. Usa CADState.java desde el Capítulo 12 como un ejemplo de cómo hacer esto. 2. (Proyecto de período) Tomando a FieldOBeasts.java como un punto de partida, construye un sistema de simulación de tráfico del automóvil. 3. (Proyecto de período) Usando a ClassScanner.java como un punto de partida, construye una herramienta que señala métodos y campos que están definidos pero nunca usados. 4 . (Proyecto de período) Usando a JDBC, construye un programa de la gerencia de contacto usando una base de datos de archivo plano conteniendo nombres, direcciones, números de teléfono, direcciones de correo electrónico, etc. Deberías poder fácilmente añadirle los nombres nuevos a la base de datos. Al introducir el nombre para ser buscado, usa terminación automática de nombre como se muestra en VLookup.java en el Capítulo 16.