When I created this blog, a little over two weeks ago, was in order to have a place for having gathered all the solutions I’m finding to my informatic doubts or problems that sometimes arise, so then I could consult them in the future (it’s not the same understanding something explained by others, as something written by and for yourself) and, incidentally, that it could also be useful to other persons who need it.

I started writing this blog in Spanish for many reasons. Among them, there can be highlighted three main ones. The first one, for love to my mother tongue. I know that in an increasingly globalized world, especially in the IT field, it is not the fashion, but I think that, in some moment and in a certain way, we all should do a little tribute and a defense of our culture, especially in these times; I decided to do it by writing a blog about informatics for Spanish people.
And this leads me to the second reason why I opted for this language. For many reasons, we Spanish people, in general, have not ever been high-skilled in English language. Before I was not it either (and I admit that nowadays I still commit gramatical errors when writing), but now I realize how incredibly limited I was when I had to search for any information through the Internet in the Spanish language. It didn’t really seduce me the fact of spending more time reading a dictionary than reading that stuff I really wanted to read. For that reason I decided that, with my blog, I could provide my little contribute to all those Spanish persons who are in the same situation I was before.
The third reason is that most of (good) documentation is written in English, not in Spanish, and this is another of the aspects I wanted to do my bit.

From now this blog will be written entirely in English. I have been thinking about the set out in the last paragraph and I have changed my mind. Regarding the first reason, although I maintain what I explained, I have to say that Internet is what it is thanks to its globality. This means that the core of the internet is people, and since the communication has been the most important humanity’s invention, I think that we have to keep on promoting it (by using a common language), making easier the WWW’s growth.
I don’t forget Spanish people. Precisely because of that I will write in English too. We need to be more competitive, and my feeling is that, at least in the linguistic field, we have been always a bit lazy. I find it great that we want to read stuff in our own language (I prefer it too), but never under the pretext that we are not capable to understand (well) any other language.

If the whole internet had been written in my language, that would have meant that the world, by extension, runs fine with just Spanish, and then I wouldn’t have needed to train myself in other languages, with the consequent self-impoverishment. Thus I’m thankful for the fact that, at least for most people’s perspective, Internet runs in a foreign language.

Regarding the documentation issue, I have noticed something. It’s true that there is more good documentation written in English rather than good documentation written in Spanish but, with time, this difference is being smaller thanks to all those persons who, voluntarily, work hard for achieving translations in their respective languages (thank you! :D). Nevertheless, as I have said, if I stop writing in Spanish is because I think globality has been the stronghold of Internet and the basis of its development, so we should mantain it when possible (observe that I don’t use the word “always”).
More: an important percentage of English written data comes from support forums: there is not really more data coming from third sources than data coming from primary sources.
And the third reason leads me to the second big subtopic of this post: the approach change of the blog.

Yes, I will keep on talking about informatics, but not in the same way I have been doing until now. At least not always.
Something that has always fascinated me is the explanation of stuff behaviour and, by what I perceive, Internet is plenty of how-to explanations, but not of the why-to ones. Most of this good documentation mentioned above belongs to the first group, and the good documentation that belongs to the second one, neither is abundant, nor easy to find. Therefore, although my blog is written in English, I will try to not make it to be just one more of the Net, it is to say, another set of how-to’s. I would like to provide something more, something new.

Most probably I won’t receive so many visits, because living in a period where most of IT practitioners need to produce at a frenzied pace, Google is used more for searching how-to’s rather than why-to’s (and it is logical), so I will be happy if persons who come here can really enjoy of the reading of this site.

Finally, I want to greet from now all the future followers of this blog and to thank you just for your visit! I hope you enjoy of it as much as I do writing here. 🙂

Dani

Advertisements

Esta es la continuación de la primera parte.

Ya sabemos lo que es un camino aleatorio, lo que es un fractal y de qué manera establecerán los números aleatorios generados por Java la dirección en la que se mueve nuestro bicho por el tablero de GridWorld.

El código fuente que contiene toda la acción (es decir, las llamadas a las funciones que se encargan de mover al bicho o de cambiar su dirección) se llama BugRunner.java. El código en su forma original está copiado en el primer post del blog. Lo único que hacía ese código era crear un tablero, crear un bicho, una roca (que no hace nada, solo está ahí para estorbar al bicho), incorporarlos al tablero y mostrarlo.
Ahora haremos más cosas. Primero que todo pegaré el código tuneado:

import info.gridworld.actor.ActorWorld;
import info.gridworld.actor.Bug;
import info.gridworld.actor.Rock;
import info.gridworld.grid.UnboundedGrid;
import info.gridworld.grid.Location;
 
public class BugRunner
{

	public static void justMove(Bug anyBug){
		if (anyBug.canMove()){
			anyBug.move();
		}
		else{
			anyBug.turn();			
		}
	}


	public static void randomBug(int n, Bug bug_three){
		double result;
		int i = 0;

		for(i = 0; i < n; i++){
			result = Math.random();
			if(result < 0.25){
				bug_three.setDirection(Location.NORTH);
				justMove(bug_three);		
			}
			else if((0.25 <= result) && (result < 0.5)){
				bug_three.setDirection(Location.EAST);	
				justMove(bug_three);	
			}
			else if((0.5 <= result) && (result < 0.75)){
				bug_three.setDirection(Location.SOUTH);		
				justMove(bug_three);
			}
			else{
				bug_three.setDirection(Location.WEST);	
				justMove(bug_three);	
			}
		}
	}	

    public static void main(String[] args)
    {
        int steps = 500000;
        ActorWorld world = new ActorWorld(new UnboundedGrid());
		Bug redBug = new Bug();
		world.add(redBug);
		randomBug(steps, redBug);
        world.show();
    }
}

Salvo quizá la función randomBug(), no parece muy complicado intuir qué es lo que hace la primera función, ¿verdad? Ésta simplemente recibe un objeto llamado Bug (que previamente creamos en la función principal, main) y decide qué es lo que hace el bicho según si éste tiene espacio para moverse en una dirección o no. La función canMove() evalua si el bicho puede avanzar una casilla en la dirección a la que apunta su cabeza; si puede, se ejecuta el método move() (no voy a ofenderte explicándote qué hace esta función), en caso contrario, el bicho gira 45º en sentido horario.

El cómo están escritos los métodos canMove(), turn() y move(), es lo de menos aquí. Lo único que hay que saber sobre éstos es que son métodos definidos en el objeto Bug (o sea, nuestro bicho), que a su vez hereda los métodos de la clase Actor, que es la que define todos los “protagonistas” de GridWorld (como los bichos, las rocas, o las flores que va dejando el bicho detrás suyo cada vez que avanza una casilla).

Ahora vamos a mirar la función randomBug. Si habéis atendido a las explicaciones dadas en el primer post, no hay mucho más que añadir, simplemente que antes de dar cada paso, redefinimos la dirección a la que apunta el bicho (la probabilidad de que apunte a una dirección u otra es, además, la misma) y a continuación llamamos justMove().

Finalmente, también hay algunos cambios en la función principal, como que en lugar de crear un tablero con los bordes definidos (como teníamos por defecto en la función principal) creamos un tablero sin bordes, esto es, infinito. Evidentemente esto es necesario para poder apreciar las formas tan extrañas que pinta el bicho (en forma de flores que va dejando por su paso).
Además de eso he quitado la roca que molesta, ya que la gracia de un camino aleatorio es que nada altere la partícula que lo traza.
Se crea el objeto redBug y se lo pasamos a la función randomBug, que hace el resto. Además, si nos fijamos, necesitaremos que el bicho se mueva muchas veces, ya que si solo avanzamos una casilla, tampoco podremos apreciar nada. Para eso definimos una variable que recibe un número y la pasamos también por valor a la función randomBug, que es la que utilizará en el bucle como cota superior y la que, por lo tanto, definirá el número de pasos que dará el bicho. Yo le he puesto medio millón de pasos. ¡Vaya caminata va a pegarse! 😀

También he modificado la función zoomOut() de la clase GridPanel, ya que la interfaz gráfica no me permetía alejarme del tablero todo lo que yo quería:

public void zoomOut()
{
    /* cellSize = Math.max(cellSize / 20, MIN_CELL_SIZE/20); */
 
    cellSize = cellSize / 10;
    revalidate();
}

Lo que está en ese comentario es la línea que había antes de que yo la sustituyera por

cellSize = cellSize / 10;

que es como funciona la función antagónica, zoomIn(), solo que ésta en vez de engrandecer el tablero diez veces lo hace simplemente el doble.

Ejecutamos el programa primero con la vista por defecto y después con el zoom:

Click para ampliar

Click para ampliar

No, no he puesto la pantalla negra del terminal ahí en medio para molestar, sino para que el bicho sea más fácil de ver. ¿Veis donde queda la cruz del botón anaranjado del terminal? Pues justo a la izquierda está el bicho. Y si os fijáis en la última línea del terminal, aparece las coordenadas del bichito. Simplemente he añadido esto en la penúltima línea en la función main:

System.out.println("Location: " + redBug.getLocation());

Porque imaginaos el panorama. Si yo ya tenía poca paciencia para encontrar a Wally entre quinientos muñecajos, imaginaos lo que supone buscar una mariquita entre medio millón de flores… 😛

Alejamos el zoom:

Click para ampliar

Click para ampliar

Súperzoom (las imágenes se verán más claras al darle con la lupa encima):

Click para ampliar

Click para ampliar

Bonito, ¿eh? 😀 Vamos a desplazarnos un poco a la izquierda del mapa:

Click para ampliar

Click para ampliar

Y ahora una ejecución más del mismo programa, directamente con el zoom a tope, solo para ver qué otras formas pintamos:

Click para ampliar

Click para ampliar

Click para ampliar

Click para ampliar

¿No resulta increíble que todas esas formas resulten de una sola función que devuelve números aleatorios? 🙂

Ahora es un buen momento para señalar un aspecto más de los caminos aleatorios. Resulta que en Física podemos modelar los gases como un conjunto muy vasto de partículas que se mueven a altas velocidades, chocando entre ellas, cambiando su dirección constantemente y, en definitiva, creando la sensación de que cada partícula se mueve de una forma absolutamente aleatoria.
Pero ocurre que ninguna de estas partículas se mueve de forma aleatoria, sino que está sometida a un proceso caótico, esto es, un proceso en el que una pequeña variación de las condiciones iniciales implica un cambio drástico en el comportamiento del sistema. Si tenemos una partícula encerrada en una caja, moviéndose siguiendo un patrón, podemos predecir su posición en cada instante de tiempo. También somos capaces de hacerlo con dos partículas. Pero con tres ya no (ver: problema de los tres cuerpos), al menos no de manera analítica, sino tirando ya de aproximaciones. Ocurre que en un gas no tenemos una, dos o tres partículas, sino que tenemos del orden de billones, de trillones de partículas… y todas ellas en movimiento, interactuando las unas con las otras, forman un sistema caótico. Ojo, no hay que confundir “caótico” con “aleatorio”. Un sistema caótico lo es en el sentido de que, debido a su complejidad, es imposible predecir su estado exacto en un momento dado, pero éste sigue sujeto a un conjunto reducido de leyes físicas: es determinista.

Pero ahora viene lo bueno. Resulta que los sistemas deterministas se comportan (cuidado, no confundir “comportarse” con “ser”) como sistemas aleatorios. Y aquí un ejemplo de ello:

Click para ampliar

Click para ampliar

¿Os resulta familiar? Pues no ha sido mi bicho. Esa imagen (extraída de la Wikipedia) es una representación de lo que se llama un proceso de Wiener, también llamado movimiento Browniano, que es el nombre que se le pone al movimiento (aparentemente) aleatorio que siguen las partículas de un gas, o de cualquier fluido en general. Precisamente el hecho de que los sistemas (caóticos) deterministas se comporten como si no lo fueran, ha servido de puente para que puedan ser estudiados en términos estadísticos y de probabilidades.
En particular, los caminos aleatorios sirven para modelizar los procesos de Wiener. Y lo mejor de todo es que estos procesos no quedan restringidos a la física estadística, sino que otras ramas de la ciencia como la Economía o la Biología también se sirven de los procesos de Wiener (o sea, de los caminos aleatorios) para modelar otros fenómenos, como la evolución de los precios del mercado o el flujo de moléculas en un proceso de difusión.

Esto va sobre estadística, fractales… y belleza. 😀

Antes que nada haré un pequeño resumen de lo que será este post así como de su organización, ya que este es un tema un tanto complejo por el contenido de otras disciplinas que involucra y que se relacionan entre si.

Este post tiene dos partes: la primera, ésta, que es donde definiré el problema a resolver, el razonamiento a seguir, y alguna consideración matemática más (algunos lo llamarán “la parte aburrida”). La segunda parte es donde echaré mano al código del programa y se verá la acción (ésta será “la parte chula”).

Como dice el título, el objetivo es, de alguna manera, representar gráficamente un fenómeno, llamado camino aleatorio, que se da cuando una partícula se mueve aleatoriamente en un espacio de N dimensiones (en nuestro caso particular, el espacio será de dos dimensiones). Esta manera será via una interfaz gráfica que proporciona el programa GridWorld, en la que inicialmente tenemos un bichito moviéndose discretamente en línea recta (mientras no haya obstáculos por su camino, como piedras u otros bichitos) por un plano dividido en cuadrículas.
A propósito, cuando digo “moviéndose discretamente” me refiero a que el movimiento se compone por un número determinado de pasos, es decir, el movimiento no se da de manera continua, no hay “fluidez”; va a saltos. Pero esto nos viene de lujo porque a la hora de demostrar (o mejor dicho, de convencernos sobre) algunas de las propiedades que exhiben los caminos aleatorios nos facilitará bastante la vida.

Obviamente tendremos que modificar el comportamiento de este bicho para que, en lugar de moverse en línea recta, se mueva de forma totalmente aleatoria. Con un número de pasos lo suficientemente grande y con la escala adecuada, veremos generado lo que se llama un fractal aleatorio. ¡Que no cunda el pánico! La traducción al castellano de todo esto está por llegar.

Finalmente añadiré que, como seguramente más de uno habrá podido sospechar, cada uno de estos pasos son decididos mediante una función de Java — Math.random() — que devuelve números aleatorios comprendidos entre el 0 y el 1 (sin llegar a éste). La idea es dividir este rango de valores en cuatro intervalos de manera que, según si el número aleatorio cae en un intervalo u otro, el bicho del programa gira cero, una, dos o tres veces 90 grados en sentido horario. Por lo tanto, si queremos que el bicho se mueva de forma aleatoria, nos interesa que los números devueltos por Math.random() sean realmente aleatorios.

Y esto nos lleva, por fin, al principio del post. Es necesario hacer una matización: los números aleatorios generados por un ordenador son pseudo-aleatorios. Vamos, que no son aleatorios. Hay que recordar que un ordenador es una máquina determinista y, como tal, no conoce el libre albedrío. En particular, no sabe generar números de forma realmente aleatoria. En su lugar utiliza funciones y parámetros tales (como los obtenidos del reloj de la CPU) que le permiten obtener números que, al menos a simple vista, aparentan ser aleatorios.

Para comprobar qué tan aleatorios son estos números generados por la máquina (o qué tan bien simulan serlo) existen los llamados tests de aleatoriedad. Hay muchos (como los especificados al final de esta página) y para diferentes propósitos. Por ejemplo, es posible que un conjunto de números supere un determinado test de aleatoriedad pero no otro.
Como decía más arriba, para asegurarme de que, al final de todo, consigo pintar un camino aleatorio, nunca está de más someter a alguna que otra prueba funciones como Math.random().

Tengo que confesar que la Estadística nunca ha sido mi fuerte, así que para convencerme de que puedo confiar en esta función he optado por un método estadístico algo más rudimentario, como es el de comprobar que, llamando Math.random() un número grande de veces, puedo obtener algo que se parezca a una distribución de Gauss. El razonamiento es el siguiente:

En cualquier ciencia experimental, una manera de acotar los errores aleatorios que surgen de la medición de una determinada magnitud consiste en repetir dicho experimento (es decir, dicha medición) un número muy grande de veces. Cuantas más veces se repita la medición, más probabilidades hay de acercarse al valor real de la magnitud que pretendemos medir (aunque nunca se alcanza de manera exacta).
Pensemos en el siguiente ejemplo: tenemos un dardo e intentamos lanzarlo a una diana (que huelga decir que está justo en el centro del tablero). Fallamos. Demasiado alto. Ahora tiramos dos dardos más. Volvemos a fallar: uno se ha desviado demasiado a la derecha y el otro ha ido más abajo. Ahora tiramos 20.000 dardos. ¿Os imagináis cómo queda el tablero después de 20.000 tiros? Sí, agujereado por todos los puntos.
Si dividiéramos el tablero en regiones del mismo tamaño, contaríamos aproximadamente el mismo número de puntos en cada una de ellas, ya que, libres de influencias externas (por ejemplo, que los dardos estén manipulados) la probabilidad de fallar tanto “por arriba” como “por abajo” es la misma, por lo que, conforme se va incrementando el número de veces que lo intentamos, el centro geométrico de todos estos tiros se aproxima (que no que coincide) a… ¡la diana! Justamente el centro geométrico del tablero.

En el caso del ejemplo obtenemos una distribución de Gauss, es decir, una función que tiene esta forma:

normal1

El eje horizontal representa el valor de nuestras mediciones (en el ejemplo podrían ser las coordenadas, con el origen (0,0) situado en la diana, de cada agujero que hemos hecho en el tablero), y el eje vertizal la probabilidad con que podemos obtener esos valores. En la imagen \mu representa la media de todos los valores que hemos obtenido y \sigma la varianza. Una de las propiedades más llamativas de la distribución de Gauss es que, independientemente de los valores de las mediciones, el 68% de éstas tienden a estar dentro del intervalo [\mu - \sigma, \mu + \sigma], coincidiendo el valor más probable precisamente con \mu , la media aritmética.

He utilizado esta segunda idea para comprobar que los números aleatorios generados por Math.random() (que recordemos que devuelve números naturales más pequeños que 1) forman algo parecido a una distribución normal. Y digo “algo parecido” porque el hecho de que la media aritmética de todos los números coincida justamente con la media de los dos extremos (que sería el equivalente a la diana de nuestro tablero), 0.5, no significa realmente nada, ya que además necesitaríamos comprobar que el 68% de los valores obtenidos caen dentro de [\mu - \sigma, \mu + \sigma], pero como el fin último de este post no es la estadística y tampoco se trata de aburrir (en exceso) al personal, vamos a hacer un acto de fe y vamos a asumir que la distribución obtenida de invocar muchas veces Math.random() se ajusta a la de la campana de Gauss.

Alguien podría quejarse diciendo que eso es mucho asumir, ya que podría ser que Math.random() devolviera todo el rato valores como 0.4, 0.6, 0.6, 0.4, 0.6… y la media de todo eso nos saldría muy cercana a 0.5, y es verdad, así que aquí está la prueba visual de que, al menos en un primer momento, podemos confiar en Math.random():

Click para ampliar

Click para ampliar

Ahora vamos a comprobar si la media de un conjunto de valores lo suficientemente grande, pongamos de 100.000.000 valores, se aproxima a 0.5:

Click para ampliar

Click para ampliar

(El código que hace esto lo he subido aquí.)

La primera media nos da 0.499993… y la segunda 0.50001… Tiene buena pinta. Podemos seguir adelante.
Por aburrido y doloroso que haya sido esto, era necesario para asegurarnos de que al final, utilizando esta función de Java, podemos simular un movimiento realmente aleatorio y de que, en consecuencia, los patrones que vamos viendo a grandes escalas resulten todavía ¡más increíbles! 🙂

Pero… ¿qué es un fractal y qué tiene que ver con todo esto? Aunque el tema por si mismo merece un post aparte, haré un esbozo de lo que son estas entidades matemáticas. Fractal proviene de la palabra latina fractus, que significa roto o fracturado. Esto es debido a que los fractales son objetos que no tienen dimensión entera, sino dimensión fraccionaria (de hecho, la primera definición formal que se dio para este tipo de objetos ya involucraba explícitamente el concepto de dimensión).
Una de las primeras definiciones más típicas (y amigables) que se da sobre ellos viene a ser así: un fractal es una forma geométrica que puede ser subdividada en partes, siendo cada una de ellas una copia (o una réplica muy parecida) a escala reducida del fractal entero. Esta propiedad es conocida como autosimilitud, y es la que también aparece a grandes escalas en los caminos aleatorios.

Como dicen que una imagen vale más que mil palabras, aquí podéis trastear con uno de los fractales más conocidos que hay: el conjunto de Mandelbrot. Podéis ir acercando el zoom a cada una de sus “ramas”, por ejemplo, para ver en qué consiste exactamente esto de la autosimilitud.
Un último apunte sobre los fractales. Aunque los fractales más conocidos son los descritos mediante funciones matemáticas iteradas sobre ellas mismas infinitas veces (idealmente), no hay que verlos como unos objetos exóticos que nada tienen que ver con la realidad. Véase por ejemplo el problema de cuánto mide la costa de Gran Bretaña.

Dicho esto, vámonos ya a la segunda parte. 🙂

Con la intención de avanzar en mi aprendizaje de Java, me descargué un pequeño proyecto al que poder modificar el código e ir así observando los resultados.
Encontré un programa llamado GridWorld, que además de estar expresamente destinado para este mismo fin, está muy bien documentado.

Este programa se descarga en forma de .zip. Una vez descomprimido, el directorio raíz se organiza de la siguiente manera:

$ ls -l
total 120
-rw-r--r-- 1 dani dani 68 Apr 16 2007 build.properties
-rw-r--r-- 1 dani dani 3323 Apr 16 2007 build.xml
drwxr-xr-x 3 dani dani 4096 Apr 16 2007 framework
-rw-r--r-- 1 dani dani 100063 Apr 16 2007 gridworld.jar
drwxr-xr-x 4 dani dani 4096 Apr 16 2007 javadoc
drwxr-xr-x 5 dani dani 4096 Apr 16 2007 projects

Quiero compilar el archivo ./projects/firstproject/BugRunner.java, que contiene el siguiente código:

import info.gridworld.actor.ActorWorld;
import info.gridworld.actor.Bug;
import info.gridworld.actor.Rock;

public class BugRunner
{
    public static void main(String[] args)
    {
        ActorWorld world = new ActorWorld();
        world.add(new Bug());
        world.add(new Rock());
        world.show();
    }  
}

La clave que me permitirá entender el error que obtendré cuando intente compilar esto directamente se encuentra en las tres primeras líneas, donde se están importando las clases:

im1

El compilador no encuentra las clases. Esto se debe a que, a diferencia de los de otros lenguajes, como el de C, al compilador de Java hay que especificarle dónde están las librerías (en su caso, las clases) que ha de utilizar. Para este menester tenemos la variable de entorno CLASSPATH, cuyo valor (que es la ruta de nuestras clases) es utilizado por la máquina virtual de Java para encontrar las clases definidas por nosotros. Básicamente hay dos maneras de utilizar esta variable en la línea de comandos: la primera, utilizándola como una opción de javac (el compilador que utilizamos), es decir:

$ javac -classpath la_ruta_de_nuestras_clases archivo.java

Los detalles están explicados en este enlace.

Y la segunda manera, que es la que he utilizado yo por parecerme la más natural: tratando CLASSPATH como una variable de entorno más del bash:

$ export CLASSPATH="ruta_a_seguir_1:ruta_2"

Podemos definir tantas rutas como queramos separando cada una de ellas por dos puntos. Es importante especificar “export” porque así hacemos que el valor de esta variable pueda ser utilizada en cada subproceso que invoquemos; de no exportarla, el valor de CLASSPATH estaría solo restringido a la shell, lo cual no nos interesa.
Una vez especificado el valor de CLASSPATH, que es un conjunto de rutas, el compilador empezará a buscar las clases, siguiendo el orden de cada ruta que le hemos indicado, cada vez que en el código fuente del archivo que compilemos hagamos una invocación a cada una de ellas.

En el caso que nos atañe tengo, como decía más arriba, el archivo BugRunner.java, que depende de una serie de clases. Las clases que he de buscar están en la carpeta raíz, GridWorldCode/, en el paquete de clases GridWorldCode/gridworld.jar, y, por supuesto, en el directorio donde se encuentra el mismo BugRunner.java, es decir, en GridWorldCode/project/firstProject/, ya que aquí se creará BugRunner.class.

Voy a indicárselo a CLASSPATH utilizando rutas relativas. A continuación compilaré mediante javac (por supuesto, sin utilizar la opción -classpath, pues las rutas que necesitamos ya están establecidas), y finalmente ejecutaré el programa:

im2

Y ya lo tenemos.

Puede pasar que al ejecutar un programa después de haber definido CLASSPATH y compilarlo (exitosamente) mediante javac obtengamos un

Exception in thread "main" java.lang.NoClassDefFoundError

Eso es porque, aunque el programa ha compilado, ha habido un error durante la ejecución: en particular, no se ha encontrado la clase que saldrá especificada a continuación de este error. En principio eso es debido a que la clase (o las clases) que falta no está en ninguna ruta de las introducidas en nuestra variable de entorno, por lo que habrá que revisar qué rutas faltan por definir, o incluso qué rutas están mal definidas, en CLASSPATH.

Vale decir que muchos entornos de desarrollo, como por ejemplo Eclipse, ofrecen un soporte gráfico en el que poder definir las variables de entorno.