lunes
oct152012
Preguntas para javeros curiosos: devolución de resultados en métodos
El caso es el siguiente:
"Desde una línea de mi programa, necesito ejecutar un método que devuelva varios resultados. Pero me encuentro con la problemática de que Java sólo me permite, por definición, un resultado de devolución en la definición de la función. Es decir, en el caso de una función relacionada con la gestión de liga de fútbol profesional, necesito que devuelva un listado de nombres, el identificador de un equipo de fútbol, y el nº de personas que pueden asistir al partido."
¿Qué soluciones podría tomar al respecto?
Reader Comments (29)
Hola. Este es un problema típico de principiante con la programación orientada a objetos. Simplificándolo mucho piensa en tu resultado como un objeto y no como un valor.
Devolver un objeto con toda la información.
En el mundo Java casi por convención no se suelen usan argumentos de salida, así que si los usas confundirás a quien lea tu código.
Si no quieres crear una clase nueva puedes usar algo como JavaTuples, pero yo creo que lo mejor es crear una clase que englobe esa información.
Usa javatuples. Puedes incluirlo como dependencia en Maven. Puedes devolver hasta diez valores con tipos personalizados.
¿Qué hace una pregunta de foro en la portada?
El caso que se propone me parece bastante trivial y carente de interés
Insisto, ya que la primera vez se censuró mi comentario: sigue habiendo votación para la publicación de noticias?
Porque da mucho que pensar que noticias así sean publicadas.
Buena respuesta de @zemi. Disculpa, @choces, que eliminé por error el comentario. Bueno, repito la pregunta, ¿qué devolvería el siguiente método por CONSOLA?
(Por cierto @choces, mi pregunta en exclamativo suena un poco borde, ¿verdad? Notas sobre principios de usabilidad, no abusar de las mayúsculas para parecer agresivo)
Y la complicamos un poco más....
public class Clase1 {
private String a = "VALORINICIAL";
public static void main(String args[]) {
int a = 5;
Clase1 b = new Clase1();
modificaValor(a,b);
System.out.println("main "+a+ " / "+b.a);
}
private static void modificaValor(int a, Clase1 b) {
a = 10;
b.a = "10";
System.out.println("modificaValor "+a+"/"+b.a);
}
}
Insisto en que un método void no devuelve nada.
No sé a dónde quieres ir a parar, aunque sospecho que se avecina una nueva serie de: ¿En Java los parámetros se pasan por valor o por referencia? ;)
A mi se me ocurren dos cosas, ambas devolver un objeto.
Puedes devolver un array de string, en el que las dos primeras posiciones sea el identificador del equipo y el número de personas a asistir, y la tercera posición, un array con el listado de nombres.
Desventaja: tendrás que tratar adecuadamente el array donde lo vaya a recibir, y a nada que quieras cambiarlo un poco, te encontrarás con que tendrás que cambiarlo en todas las partes que llame al método y se puede complicar.
Una mejor solución para mí, es crear una clase para eso. Dependiendo del programa, puedes hacer una clase "estandar" que pueda servir para todos los métodos que necesite devolver más de una cosa.
Intentar que devuelva más de un método sería complicarse bastante, lástima no tener las struct de C, aunque vendrían a ser las clases en Java.
Devuelve un HashMap (: y listo
Un método void puede imprimir valores por consola, por lo que puede devolver información a la consola. Otra cosa es que no devuelva información al método que lo llamó, que son dos cosas diferentes.
La profecía que realizas es correcta, en Java los tipos primitivos se pasan por copia, sin embargo, las instancias de una clase (un objeto), se pasan por memoria, por lo que se pueden modificar.
Si yo paso un int como argumento, siempre es un argumento de entrada (al ser una copia del valor) sin embargo, si yo paso una instancia de un objeto (un arraylist, por ejemplo), puedo modificar esa entrada en tiempo de ejecución, añadiendo y eliminando valores.
Por lo que, la respuesta de @zemi no responde a valoraciones técnicas, si no a una convención, que no sé si responde a su criterio particular (un criterio suyo, o bien de un grupo reducido de personas) o bien a un criterio marcado por Oracle. Los parámetros son de entrada, sin embargo, puedo modificar valores pasados como argumentos, si no son tipos primitivos.
Saludos
¿Para estas cosas no está el foro?
En general estoy con zemi. Lo suyo para lo que necesita es crearse una clase "resultado" o bien que el método devuelva un Map, etc.. con los resultados que necesita.
Así con tu MAP, tienes resultado("Identificador"), resultado("numeroPersonas"), etc... Pero vamos que lo que necesitas es una clase tipo "DatosPartido", que tenga como atributos los datos que necesitas.
Lo de poner argumentos de "salida" me recuerda a PLSQL, o Pascal...., pero hasta en Pascal o C se devolvía un *Struct .., aunque sí eran comunes los argumentos de salida.
Saludos
No mezclemos conceptos:
"The result of a method declaration either declares the type of value that the method returns (the return type), or uses the keyword void to indicate that the method does not return a value."
http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.4
Una cosa es que un método devuelva o no un valor, y otra diferente que desde el bloque de código del método se puedan modificar los contenidos de los objetos que se pasan como parámetros.
En general, me parece un práctica arriesgada, como poco.
Por otra parte, si se declara un método así:
public void setList(final List<String> list){
list=new ArrayList<String>(); // ¡Error de compilación!
}
Por lo que no siempre se puede alterar la declaración de un parámetro dentro del método que lo invoca.
En realidad todo se pasa por valor, lo que pasa es que cuando pasas un objeto, lo que se pasa por valor es la referencia al objeto, que no se puede modificar, y a través de ella se puede modificar el contenido.
Y como dice choces, una cosa es lo que devuelve un método, y otra cosa es imprimir por pantalla. Ya que se supone que somos técnicos, lo mínimo es usar nomenclatura no ambigua ;).
Y como dice también, es más claro que lo que se modifique sea lo que se devuelva, y no los parámetros, aunque ya sabemos que no siempre se cumple. Principio de mínima sorpresa, evitar efectos colaterales... esas cosas.
De todas formas, ¿este tipo de cosas no serían más adecuadas en alguno de los foros? No me parece una noticia.
Vamos a ver:
1) Nadie ha comentado que un método void devuelva un valor, estamos diciendo que envía información a consola (aunque se haya utilizado la palabra devolver). No nos desvíemos de la cuestión.
2) Por otro lado, si yo ejecuto el siguiente código:
import java.util.ArrayList;
import java.util.List;
public class Clase1 {
public static void main(String args[]) {
int valor = 1;
List<String> lista = new ArrayList<String>();
System.out.println("Valor inicial "+valor);
System.out.println("Tamaño original " + lista.size());
modificaValor(valor, lista);
System.out.println("Valor final " + lista.size());
System.out.println("Tamaño final " + lista.size());
}
private static void modificaValor(int valor, List<String> lista) {
valor = 3;
System.out.println(valor);
System.out.println(lista);
lista.add("VALOR 1");
}
}
Veo la siguiente salida (el método devuelve la siguiente información por pantalla, @choces),
Valor inicial 1
Tamaño original 0
3
[]
Valor final 1
Tamaño final 1
En este caso, se puede utilizar parámetros como salida (modifico los parámetros). La pregunta es, ¿qué opina Oracle o bien Sun, sobre esta práctica? Técnicamente es posible, ¿pero desde un punto de vista formal es un error?
Sobre tu aportación, podría hacer lo siguiente:
private static void modificaValor(int valor, final List<String> lista) {
valor = 3;
System.out.println(valor);
System.out.println(lista);
lista.add("VALOR 1");
}
Y alterar no la posición de memoria, pero sí las posiciones adyacentes.
¿Qué opináis entonces?
Otra cosa curiosa es la siguiente:
private static void modificaValor(final int valor, final List<String> lista) {
valor = 3;
System.out.println(valor);
System.out.println(lista);
lista.add("VALOR 1");
}
Este método no compila porque indico que, la variable valor, pasado como copia, es de tipo final, lo que impide que, una vez realizada la copia, se modifique el valor dentro del método, aunque, en cualquier caso, la variable original pasada no iba a ser modificada.
Saludos
@jcarmonaloeches creo que estas confundiendo las cosas, una cosa es el tipo de retorno y otra muy diferente que java como lenguage tenga diferentes mecanismos de entrada y salida, por que desde tu punto de vista que java pueda escribir un xml o un texto o lo que sea es un tipo de retorno y eso es absolutamente incorrecto.
Veamos algunas puntos claves de un constructor: "Two key points to remember about
constructors are that they have no return type and their names must exactly match
the class name. [SCJP Sun® Certified Programmer for Java™ (Kathy Sierra - Bert Bates)]" . "The first thing to notice is that constructors look an awful lot like methods. A
key difference is that a constructor can't ever, ever, ever, have a return type…ever![SCJP Sun® Certified Programmer for Java™ (Kathy Sierra - Bert Bates)]" Claramente (y con enfasis) dice: que un constructor nunca nunca jamas tendra un tipo de retorno... aunque obviamente en un constructor podemos poner un Systema.out... lo cual en definitiva no quiere decir que retorne un valor.
Otro punto los metodos sobreescritos tienen la siguiente restriccion: "The return type must be the same as, or a subtype of, the return type declared in the original overridden method in the superclass.[SCJP Sun® Certified Programmer for Java™ (Kathy Sierra - Bert Bates)]". El Tipo de retorno de un metodo sobre cargado, debe ser el mismo o un subtipo del tipo de retorno del metodo original. Entonces no podria extender la clase Date y sobreescribir el metodo getDay(); para que en lugar de retornar un entero retorne por pantalla un "ALGO"... por que el tipo de retorno no cambia... en cambio si puedo extender Date y sobreescribir el metodo getDays(), y enviar a la salida estandar cualquier cosa que yo quiera... y despues retornar el mismo int.
Si métodos e instancias estén dentro de la misma clase, usar los métodos de esa manera no tiene mucho sentido, porque se pueden modificar las instancias directamente.
El problema aparece cuando se trata de clases diferentes: en una están las instancias y en otra los métodos que las modifican. Para ello, esas instancias deben declararse como públicas, lo que, de entrada, rompe por completo la encapsulación, al hacerlas no solo visibles, sino mutables por métodos ajenos a la clase en la que se declaran.
Por eso comentaba más arriba que es una práctica arriesgada, por peligrosa; todo ello sin entrar en la cuestión de cómo hacer que esos métodos e instancias sean thread-safe.
necesito que devuelva un listado de nombres, el identificador de un equipo de fútbol, y el nº de personas que pueden asistir al partido
La propia pregunta ya denota que no se esa pensando en el nivel de abstracción adecuado. Si queremos hacer OO tenemos que pensar en objetos y usar abstracción y dejar los detalles para donde corresponda. Lo que en realidad necesitas es que alguien te devuelva la lista con los partidos (o algo similar, el ejemplo no me deja claro que quieres). Planteando así la funcionalidad queda claro que lo que vas a devolver es un List<Partido>. Y el partido ya se encargará saber quien juega (equipos), donde juega (será el estadio el que tenga el número de asistentes ¿verdad?). De esta manera empiezas a construir un modelo de tu problema a resolver con sustantivos (los objetos) y verbos (las acciones/métodos).
Si se piensa en objetos es raro, raro, raro que surja jamas la necesidad de devolver "más de una cosa", menos aún de que un método modifique parametros de entrada que esto si que es peligroso, inmutabilidad my friend, es clave para hacer código mantenible.
Aunque ya puestos, lo ideal es que los métodos no devuelvan cosas sino que hagan cosas, es decir Tell don't ask, no siempre es facil seguir este principio, pero siempre conviene analizar el problema desde esta perspectiva que muchas veces terminamos usando un lenguaje OO para hacer un código más procedural que otra cosa.
Muchas gracias a tod@s por vuestra participación.
Empecé a leer el Tell don't ask, pero no he podido profundizar en el mismo. Aún así, parece el siguiente escalafón.
A ver os doy mi punto de vista sobre varias ideas que han surgido de vuestras participaciones, me centro en @choces, que Alfredo Casado confirma ideas que habían surgido previamente (además de incluir la temática Tell don't ask).
1) Si métodos e instancias estén dentro de la misma clase, usar los métodos de esa manera no tiene mucho sentido, porque se pueden modificar las instancias directamente -> 100% agree.
2) El problema aparece cuando se trata de clases diferentes: en una están las instancias y en otra los métodos que las modifican. Para ello, esas instancias deben declararse como públicas, lo que, de entrada, rompe por completo la encapsulación, al hacerlas no solo visibles, sino mutables por métodos ajenos a la clase en la que se declaran -> CIERTO, EL PLANTEAMIENTO INICIAL ROMPÍA EL PLANTEAMIENTO DE ORIENTACIÓN A OBJETOS.
3) Por eso comentaba más arriba que es una práctica arriesgada, por peligrosa; todo ello sin entrar en la cuestión de cómo hacer que esos métodos e instancias sean thread-safe -> ¿PUEDES PLANTEAR CASOS CONCRETOS, POR FAVOR? EN ESTE TEMA RECONOZCO QUE HE PROFUNDIZADO MENOS.
¿Cuál era mi problema?
No había diseñado y me había encontrado con la necesidad que, un método, almacenará valores en una lista, y posteriormente, otro método cogiera esa lista y esas modificaciones.
1) Yo, como buen cenutrio, pasaba la lista como parámetro y añadía valores.
2) Siguiendo en mi cenutriedad y la no restricción de Java ni Eclipse ante dicho hecho, pasaba el argumento a un segundo método y realizaba la misma operación (añadido de datos).
3) Después de ello, con la lista ya rellena, pasaba la lista como argumento de entrada a una tercera función que se encargaba de recorrerla, no de añadir datos.
¿Cómo lo he solucionado? FACIL: creo una variable a nivel de clase, no una variable a nivel de método, por lo que, desde los métodos, puedo acceder a esas variables.
Aún así, me planteo varias dudas:
1) En la mayoría de mi código no es necesario usar esta variable, y sin embargo, mientras la clase perdura, dicha variable ocupa memoria.
2) ¿Por qué Java no es tajante ante el hecho de que pueda modificar variables por referencia? La respuesta está en que se permite, por ejemplo, modificaEquipo (Equipo equipo1, Equipo equipo2) y permita que, con los valores de una instancia, se modifiquen los de la segunda. Es decir, yo puedo trucar el lenguaje, saltándome los principios... hehehe, y hacer programación procedural.... vaya vaya....
No sé vosotros, yo lo veo incoherente en la idea, parece que queda claro que el programador es responsable del buen uso.... pero el error de concepto de definición de regla es lo que personalmente me llama más la atención, en modo crítica constructiva.
De todas maneras, no estoy a la última en este concepto, y entiendo que tengo que ponerme con Tell don't ask....
Saludos y gracias,
Jaime
Hay un excelente tutorial, sencillo pero muy ilustrativo, de Jakob Jenkov, que te puede servir para introducirte en el asunto espinoso de la concurrencia:
http://tutorials.jenkov.com/java-concurrency/thread-safety.html
Para más profundidad, me temo que tendrás que "pelearte" con:
http://docs.oracle.com/javase/tutorial/essential/concurrency/
Respecto a tus cuestiones:
1.- Usar o no una variable como campo de una clase depende de cómo diseñes tu implementación. Con mucha frecuencia es posible reducir, sino eliminar por completo, ese tipo de variables. Cuando no se puede es porque son estrictamente necesarias; y si lo son, deben ocupar memoria inevitablemente.
2.- Java heredó desde sus inicios muchos conceptos de otros lenguajes, aparte de implementaciones "dudosas" de la especificación: véanse la gran cantidad de métodos y clases obsoletas que hay en el JDK.
Sin embargo, el que se pueda pasar una instancia por valor no deja de ser útil en algunos casos:
* Una clase abstracta puede usar métodos que tengan como parámetro un campo de la misma, de tal manera que las clases que la hereden puedan sobreescribir esos métodos que modifican los contenidos de ese campo. Si se codifica con cuidado, no debe haber problemas. Algunos decodificadores de mp3 usan esa técnica. No es mi favorita, pero funciona bien.
* La llamada Inyección de Dependencia aprovecha esa característica de la especificación, para pasar como parámetro de un método una instancia de otra clase. Sin embargo, su objetivo consiste en acceder a los métodos de la clase parametrizada, no a sus variables internas.
> En la mayoría de mi código no es necesario usar esta variable, y sin embargo,
> mientras la clase perdura, dicha variable ocupa memoria.
Problablemente sea buena idea partir la clase donde está la variable en varias más pequeñas.
En cualquier caso, no deberías preocuparte mucho por lo que ocupan los objetos en memoria, a menos que tu aplicación corra en sistemas embebidos o que tus objetos contengan la discografía de Raphael en formato FLAC.
Gracias a todos por vuestros aportes.
Por otro lado, @Vladimircs, lo que dices está bien, pero quería decir que un método puede devolver o volcar información a consola (no tipos de retorno), que es otra cosa diferente. Quizá mi subconsciente me impidió expresarme correctamente, siendo estricto tienes razón.
Por cierto, ese libro que comentas me lo leí hace tiempo ya, y está bastante bien. Es muy útil de cara al aprendizaje del lenguaje y de cara a la certificación.
Gracias @zemi, @choces, @alfredocasado por vuestras aportaciones, quiero aprovechar estos comentarios para ir introduciendo Scala y qué aporta Scala, desde mi punto de vista, con respecto a Java poquito a poco....
Saludos,
Un apunte más, estas entradas, irán, destinadas, desde ahora, en la sección de 'certificación'. Vamos a intentar activar debates críticos y educativos sobre el lenguaje.
Saludos