sábado, 15 de diciembre de 2012

La clase BufferStrategy

votar
En una entrada anterior te había explicado cómo hacer una pequeña animación en Java. Ahora bien, es posible que te hayas dado cuenta de que no es todo lo perfecta que debiera, si no que se producen pequeños desajustes, o parpadeos, entre las imágenes, o que incluso a veces se pueden ver algo superpuestas.

Esto es así porque el programa está "dibujando" las imágenes a la vez que el sistema está continuamente refrescando la pantalla. Una forma de evitarlo es crear un espacio de almacenamiento donde el programa dibuje la imagen y no a muestre hasta que esté totalmente acabada. Exactamente esto es lo que hace la clase BufferStrategy. Además, se encarga que las imágenes no se muestren más rápido de la velocidad con la que el monitor se refresca, es decir, supongamos que tú tienes un juego en el que aparecen 150 escenas por segundo, pero tu monitor funciona a 75Hz, lo que significa que sólo puede mostrar 75 escenas por segundo, pues la clase BufferStrategy adapta el programa a esta velocidad. 

Lo bueno es que tú sólo tienes que implementarla, ella solita se encarga de manejar los recursos del sistema. Se puede utilizar tanto con Canvas como con Windows, y para crearla elige primero el número de lugares de almacenamiento o "buffers" que deseas y luego utiliza el método createBufferStrategy(), por ejempo así:

ventana.createBufferStrategy(2);

donde 2 es el número de buffers que creamos. Para invocarlo usaremos el método getBufferStrategy(). Por supuesto, hay que dibujar la imagen en el buffer y luego mostrarla. Esto se hace de la siguiente manera:

BufferStrategy buffer = ventana.getBufferStrategy();
Graphics g = buffer.getDrawGraphics();
draw (g);
g.dispose();
buffer.show();

Te dejo a continuación una actualización del código visto en la entrada sobre animación, PantallaCompleta.java, donde ya está implementada la clase BufferStrategy, y ya puestos, se añaden otras mejoras, como el método getCompatibleDisplayModes(), que te ayudará a conseguir el mejor rendimiento posible del sistema que estés utilizando. También te dejo los códigos de AnimaciónTest.java y Animación.java, para que los tengas todos a mano, aunque también los tienes en la entrada correspondiente.
Si aún no sabes crear una pantalla a modo completo, visita esta otra entrada.

PantallaCompleta.java

import java.awt.*;
import java.awt.image.BufferStrategy;
import javax.swing.JFrame;
import java.awt.image.BufferedImage;

public class PantallaCompleta{
   private GraphicsDevice gd;
   public PantallaCompleta(){
      //hay que usar los medios del propio sistema
      GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
   gd = env.getDefaultScreenDevice();
 }
 
 //devuelve una lista de los modos de visualización compatibles con las características de nuestro sistema
 public DisplayMode[] getCompatibleDisplayModes(){
    return gd.getDisplayModes();
 }
 
 //devuelve el primer modo compatible o nada si no encuentra ninguno
 public DisplayMode findFirstCompatibleMode(DisplayMode modes[]){
    DisplayMode goodModes[] = gd.getDisplayModes();
    for(int i = 0; i < modes.length; i++){
     for(int j = 0; j < goodModes.length; j++){
     if(displayModesMatch(modes[i], goodModes[j])){
      return modes[i];
    }
  }
    }
    return null;
  }
  
  //devuelve el modo de visualización actual
  public DisplayMode getCurrentDisplayMode(){
     return gd.getDisplayMode();
 }
 
 /**
     Determina si dos modos de visualización coinciden, es decir,
  si tienen la misma resolución, velocidad de actualización de
  pantalla, etc.
 */ 
 public boolean displayModesMatch(DisplayMode mode1, DisplayMode mode2){
    if(mode1.getWidth() != mode2.getWidth() || mode1.getHeight() != mode2.getHeight()){
     return false;
    }
    if (mode1.getRefreshRate() !=
        DisplayMode.REFRESH_RATE_UNKNOWN &&
     mode2.getRefreshRate() !=
     DisplayMode.REFRESH_RATE_UNKNOWN &&
     mode1.getRefreshRate() != mode2.getRefreshRate()){
      return false;
  }
  return true;
 }
      
 /**
     Se pone la pantalla a modo pantalla completa y se cambia
  el modo de visualización. En caso de que no hubiese ninguno
  compatible o no se pudiese cambiar, se utiliza el que tiene
  el sistema
 */
 public void setFullScreen(DisplayMode displaymode){
    JFrame ventana = new JFrame();
    ventana.setUndecorated(true);
    ventana.setIgnoreRepaint(true);
    ventana.setResizable(false);
    
    gd.setFullScreenWindow(ventana);
    
    //comprobamos que el sistema soporta el cambio
    if(displaymode != null && gd.isDisplayChangeSupported()){
       try{
      gd.setDisplayMode(displaymode);
    }catch(IllegalArgumentException e){}
  }
  ventana.createBufferStrategy(2);
 }
 
 public Graphics2D getGraphics(){
    Window window = gd.getFullScreenWindow();
    if(window != null){
       BufferStrategy strategy = window.getBufferStrategy();
    return(Graphics2D)strategy.getDrawGraphics();
  }else{
    return null;
  }
 }
 
 public void update(){
    Window window = gd.getFullScreenWindow();
    if(window != null){
       BufferStrategy strategy = window.getBufferStrategy();
    if(!strategy.contentsLost()){
      strategy.show();
    }
  }
  
  //sincronizamos con la ventana del sistema
  Toolkit.getDefaultToolkit().sync();
 }
 
 public Window getFullScreenWindow(){
    return gd.getFullScreenWindow();
 }
 
 public int getWidth(){
    Window window = gd.getFullScreenWindow();
    if(window != null){
       return window.getWidth();
  }else{
    return 0;
  }
 }
 
 public int getHeight(){
    Window window = gd.getFullScreenWindow();
    if(window != null){
     return window.getHeight();
    }else{
     return 0;
    }
 }
 
 //restauramos los valores previos
 public void restoreScreen(){
    Window w = gd.getFullScreenWindow();
    if(w != null){
       w.dispose();
  }
  gd.setFullScreenWindow(null);
 }
 
 /**
    Creamos una imagen compatible con el modo de visualización,
    que se almacena en la memoria del sistema
 */
 public BufferedImage createCompatibleImage(int w, int h, int transparency){
    Window window = gd.getFullScreenWindow();
    if(window != null){
      GraphicsConfiguration gc = window.getGraphicsConfiguration();
   return gc.createCompatibleImage(w, h, transparency);
  }
  return null;
 }
}


Animación.java



import java.awt.Image;
import java.util.ArrayList;

public class Animación{
   private ArrayList frames;
   private int actualFrame;
   private long tiempoAnimación;
   private long tiempoTotal;
   
   public Animación(){
      frames = new ArrayList();
   tiempoTotal = 0;
   start();
 }
 
 public synchronized void addFrame(Image image, long duración){
    tiempoTotal += duración;
    frames.add(new AnimFrame(image, tiempoTotal));
 }
 
 public synchronized void start(){
    tiempoAnimación = 0;
    actualFrame = 0;
 }
 
 public synchronized void update(long tiempoTranscurrido){
    if(frames.size()>1){
     tiempoAnimación += tiempoTranscurrido;
     if(tiempoAnimación >= tiempoTotal){
     tiempoAnimación = tiempoAnimación % tiempoTotal;
     actualFrame = 0;
  }
  while(tiempoAnimación > getFrame(actualFrame).endTime){
     actualFrame++;
  }
    }
 }
 
 public synchronized Image getImage(){
    if (frames.size() ==0){
       return null;
  }else{
     return getFrame(actualFrame).image;
  }
 }
 
 private AnimFrame getFrame(int i){
    return (AnimFrame)frames.get(i);
 }
 
 private class AnimFrame{
    Image image;
    long endTime;
    public AnimFrame(Image image, long endTime){
       this.image = image;
    this.endTime = endTime;
  }
 }
}


AnimaciónTest.java


import java.awt.*;
import javax.swing.ImageIcon;


public class AnimaciónTest {

    public static void main(String args[]) {

       //DisplayMode displayMode= new DisplayMode(1024, 768, 32,
                //DisplayMode.REFRESH_RATE_UNKNOWN);
        AnimaciónTest test = new AnimaciónTest();
        test.run();
    }
    private static final DisplayMode POSSIBLE_MODES[]={
     new DisplayMode(800, 600, 32, 0),
        new DisplayMode(800, 600, 24, 0),
        new DisplayMode(800, 600, 16, 0),
        new DisplayMode(640, 480, 32, 0),
        new DisplayMode(640, 480, 24, 0),
        new DisplayMode(640, 480, 16, 0)
    };

    private static final long DEMO_TIME = 10000;

    private PantallaCompleta pc;
    private Image bgImage;
    private Animación animación;

    public void loadImages() {
        // cargamos las imágenes
        bgImage = loadImage("images/background.jpg");
        Image cara1 = loadImage("images/cara1.png");
        Image cara2 = loadImage("images/cara2.png");
        Image cara3 = loadImage("images/cara3.png");

        // creamos la animación
        animación = new Animación();
        animación.addFrame(cara1, 250);
        animación.addFrame(cara2, 150);
        animación.addFrame(cara1, 150);
        animación.addFrame(cara2, 150);
        animación.addFrame(cara3, 200);
        animación.addFrame(cara2, 150);
    }


    private Image loadImage(String fileName) {
        return new ImageIcon(fileName).getImage();
    }


    public void run() {
        pc = new PantallaCompleta();
        try {
      DisplayMode displayMode = pc.findFirstCompatibleMode(POSSIBLE_MODES);
            pc.setFullScreen(displayMode);
            loadImages();
            animationLoop();
        }
        finally {
             pc.restoreScreen();
        }
    }


    public void animationLoop() {
        long tiempoInicio = System.currentTimeMillis();
        long tiempoActual = tiempoInicio;
        while (tiempoActual - tiempoInicio < DEMO_TIME) {
            long tiempoTranscurrido =
                System.currentTimeMillis() - tiempoActual;
            tiempoActual += tiempoTranscurrido;

            // actualizamos la animación
            animación.update(tiempoTranscurrido);

            // se dibuja en pantalla
            Graphics2D g =
                pc.getGraphics();
            draw(g);
            g.dispose();
   pc.update();

            // una pequeña pausa
            try {
                Thread.sleep(20);
            }
            catch (InterruptedException ex) { }
        }

    }


    public void draw(Graphics g) {
        // se dibuja el fondo
        g.drawImage(bgImage, 0, 0, null);

        // se dibuja la imagen y la centramos más o menos
        g.drawImage(animación.getImage(), 300, 200, null);
    }

}


Si quieres ver más a fondo la clase BufferStrategy, puedes visitar estas páginas:


Vale, están todas en inglés, así que, si no lo dominas, es una suerte que esté yo, ¿eh? ;-)

domingo, 2 de diciembre de 2012

Introducción a los genéricos (generics) en Java

votar
Érase una vez, en una galaxia muy, muy lejana, antes de Java 5 y de la existencia de genéricos, los programadores java que utilizaban colecciones, se enfrentaban a un temible peligro.

Ellos podían, por ejemplo, crear una lista de Strings como la siguiente:

List miLista = new ArrayList( );
miLista.add(“Hola”);
miLista.add(“Galaxia”);
miLista.add(new Integer(2001));

podían, incluso, hacer un casting al obtener los elementos de esa lista:

String s = (String) miLista.get(0);
String st = (String) miLista.get(1);
String str = (String) miLista.get(2);

¡Pero nada de eso evitaba que al ejecutarse el programa se produjera un error! ¿Por qué? ¡Porque el tercer elemento no es un String, sino un Integer, pero nada impedía añadirlo a la lista! Por suerte, los genéricos vinieron al rescate de estos desdichados programadores y nunca más se produjeron errores de este tipo.

Bueno, vale, puede que la historia no fuese exactamente así, pero nos sirve para explicar en qué consisten los genéricos y su función. Desde luego los genéricos no se utilizan sólo con las colecciones, pero es cierto que es en ellas donde te los vas a encontrar mayoritariamente.

Los genéricos permiten asignar parámetros a las clases, interfaces, métodos..., de forma que sólo admitan los tipos de objetos que tu quieras. No se pueden utilizar con primitivos, pero si con las clases que se corresponden con ellos. Por eso no puedes hacer un genérico tipo int, pero sí un Integer. Veamos el ejemplo anterior utilizando genéricos:

List <String> miLista = new ArrayList <String>();
//al poner <String> estamos indicando que la lista sólo acepta Strings
miLista.add(“Hola”); //un String, perfecto
miLista.add(“Galaxia”); //otro String, muy bien
miLista.add(new Integer(2001)); //¡error de compilación, no es un String!

Evidentemente, es mucho más sencillo solucionar un error de compilación que un error de ejecución. Además, no hace falta ningún casting para obtener los elementos de la lista:

String s = miLista.get(0);
String st = miLista.get(1);
//el compilador ya sabe que es un String, no hace falta casting.

Para conseguir esto, basta poner entre los signos < >, llamados diamante, el tipo al que queremos que pertenezcan los elementos: String, Integer o lo que sea. Además, para facilitar más las cosas, a partir de Java 7 la primera línea de este código se escribiría así:

List <String> miLista = new ArrayList<>();

porque el compilador ya infiere de qué tipo son los elementos de la lista.

Pero, ¿cómo crear nuestra propia clase genérica? Fíjate en este ejemplo:

public class Cajón<T>{
   private T t;
   public void set (T t){
   this.t = t;
   }
   public T get( ){
   return t;
   }
}

Ya hemos construído una clase genérica, y le hemos proporcionado dos métodos. La T es el tipo de parámetro que le vamos a asignar. Supón por ejemplo que quieres un Cajón de Calcetines y otro Cajón de Camisetas, pues sustituyes T por Calcetines o por Camisetas (por supuesto, si no existen las clases Camisetas ni Calcetines, debes crearlas) de esta forma:

public class Cajón<Calcetines> o public class Cajón<Camisetas>

Todos los métodos de tu clase genérica pueden ser utilizados en cualquiera de ellas, pero no podrás usar calcetines con Cajón<Camisetas> ni camisetas con Cajón<Calcetines>.

Por convención, los nombres de los tipos de parámetros se escriben como una letra mayúscula, y los más habituales son :
  • T, de tipo
  • E, de elemento, muy usado con la Java Collection Framework
  • N, de número (class Number)
  • K, de key
  • V, de value, estas dos últimas utilizadas sobre todo en mapas (Map)

¿Y cómo sería un método en el que estemos utilizando genéricos?

void añadirCalcetines (Cajón<Calcetines> cal){
   cal.add(new Calcetines( ));
}

Un método muy simplón, pero sirve para que veas la sintaxis. Y hablando de sintaxis, es muy habitual con los genéricos ver esto: <?> acompañado a veces de “extends” o “super”. Ahora mismo te explico lo que significa. Supongamos que tienes una lista de números enteros:

List<Integer> miLista = new List<>( );

y piensas, “bueno, Integer es una subclase de Number, así que puedo hacer esto” y escribes:

List<Number> miLista = new List<Integer>( );

¡No compila! ¿Por qué? ¡Porque List<Integer> no es un subtipo de List<Number>! En List<Number> sólo pueden entrar objetos que pertenezcan a la clase Number, no objetos de la clase Integer, ni de la clase Double, ni Short, ni Long. Sólo de la clase Number, por eso, aunque a la lista puedes añadirle números enteros, o decimales o los que quieras que pertenezcan a Number, no puedes decir que es una nueva lista de números enteros. Ahora bien, si escribimos esto:

List<? extends Number>

estamos indicando que esta lista va a admitir objetos de la clase Number y de cualquier otra que la extienda (“?” significa cualquiera). Así pues, modificamos

List<?extends Number> miLista = new List<Integer>( );

y ahora sí que compila. Y también compilará si en lugar de List<Integer> utilizamos cualquier otra subclase de Number, como List<Double>.

Pero, ¿qué ocurre si lo que queremos es lo contrario, es decir, que la lista admita objetos de la clase Integer y su superclase, pero no ninguna otra subclase de Number?
En ese caso, haríamos lo siguiente:

List<? super Integer>

con lo que la lista admitirá objetos de la clase Integer y de cualquier otra clase superior a ella, como Number.

¿Y qué ocurre si hacemos esto?:

List<?>

Lo que estamos indicando ahora es que la lista admitiría cualquier tipo de objetos. No confundir con “objetos de la clase Object”. Es decir, esto:

List<Object>

sólo admite objetos que pertenezcan a la clase Object, y no podemos hacer lo siguiente:

List<Object> miLista = new List<Calcetines>( ); //no compila

Por último, quiero añadir que es cierto que los genéricos en Java no son perfectos (como los programadores en C siempre señalan amablemente) pero eso es así porque desde un principio se busco que fuese compatible con todo el código escrito con anterioridad a Java 5, que no los utilizaba. No era cuestión de que todo ese código dejase de funcionar, ¿no?

Si has tenido dificultades en seguir esta entrada porque no sabes lo que son las colecciones, puedes ver esta entrada anterior. Si el problema es con el polimorfismo y la herencia, consulta esta otra. Para ampliar conocimientos, consulta los tutoriales de Oracle.

Si tienes dudas, pregúntame. Y si te ha gustado, compártelo.;-)


En respuesta a la consulta de Anet (ver Comentarios)
Hola, Anet:
¡Es un placer ver una chica por aquí!
Respecto a tu pregunta, no tengo muy claro qué es exactamente lo que quieres hacer, así que no sé si la respuesta será la que buscas.
Como ya hemos visto, los genéricos siguen las reglas de herencia de Java a nivel de tipo, pero no a nivel de parámetro.
Por ejemplo, podemos hacer

Set<String> mySet = new HashSet<String>();

porque HashSet es un subtipo de Set, pero no

Set<Object> mySet = new HashSet<String>();

porque sólo puedes usar Object como parámetro, no String, y por eso utilizamos los wildcards y extends, como muestro arriba.
Con las interfaces, sería lo mismo, solo que en lugar de extends, usaríamos implements.

A continuación te dejo un ejemplo de un código, donde creo una clase que va a ser un subtipo de ArrayList. Fíjate que por eso no necesitamos que mi clase implemente la interfaz List, porque eso ya lo hace ArrayList.
Además, vamos a hacer que implemente una interfaz que yo he creado, de esa manera podrás ver cómo se hace.

import java.util.*;
public  class TestGen{

    public static void main(String args[]){
   MyGen<String> miLista = new MyGen<String>();
miLista.add(0,"A");
miLista.add(1,"B");
miLista.add(2,"C");
for (String item: miLista){
System.out.println(item);
}
ArrayList<String> miOtraLista = new MyGen<String>();
miOtraLista.addAll(miLista);
for (String item: miOtraLista){
System.out.println(item);
}
if (miOtraLista.containsAll(miLista)){
System.out.println("Tienen los mismos elementos");
}
String miEjemplo = miLista.ejemplo().toString();
System.out.println(miEjemplo);
}

}
//La clase MyGen extiende ArrayList (que a su vez implementa List) y además implementa la interfaz //Ejemplo
class MyGen<String>  extends ArrayList<String> implements Ejemplo<Double>{
//aquí sustituímos T por Double, pero podríamos poner cualquier otro tipo
public MyGen(){
super();
}
//nuestra versión del método ejemplo
@Override
public Double ejemplo(){
return 15.25;
}
}
//creamos la interfaz usando T, de forma que cuando la implementemos podamos usar el tipo que //queramos.
 interface Ejemplo<T>{
T ejemplo();
}

De esta manera, la clase MyGen puede utilizar los métodos de ArrayList, que a su vez tiene los de las interfaces que implementa, y además va a poder usar el método de la interfaz Ejemplo.
Espero que ésto te ayude a aclarar los conceptos.

¡Un saludo!