java - interfaz - ¿Podemos combinar dos métodos que difieren en gran medida en función del tipo?
java swing tutorial pdf español (7)
Tengo dos bloques de código similares pero de diferentes tipos en Java:
private Integer readInteger() {
Integer value = null;
while (value == null) {
if (scanner.hasNextInt()) {
value = scanner.nextInt();
} else {
scanner.next();
}
}
return value;
}
private Double readDouble() {
Double value = null;
while (value == null) {
if (scanner.hasNextDouble()) {
value = scanner.nextDouble();
} else {
scanner.next();
}
}
return value;
}
¿Es posible hacer solo un método que funcione para ambos?
Esto es sobre la duplicación de código .
El enfoque general es convertir un código similar (tiene) en un código igual que se puede extraer a un método parametrizado común.
En su caso, lo que hace que los dos códigos cortados difieran es el acceso a los métodos de Scanner
. Tienes que encapsularlos de alguna manera. Sugeriría hacer esto con las interfaces funcionales de Java8 como esta:
@FunctionalInterface
interface ScannerNext{
boolean hasNext(Scanner scanner);
}
@FunctionalInterface
interface ScannerValue{
Number getNext(Scanner scanner);
}
Luego reemplace la invocación real de métodos en el escáner con la interfaz funcional:
private Integer readInteger() {
ScannerNext scannerNext = (sc)->sc.hasNextInt();
ScannerValue scannerValue = (sc)-> sc.nextInt();
Integer value = null;
while (value == null) {
if (scannerNext.hasNext(scanner)) {
value = scannerValue.getNext(scanner);
} else {
scanner.next();
}
}
return value;
}
Hay un problema más que difiere del tipo de la variable de valor . Entonces lo reemplazamos por su supertipo común:
private Integer readInteger() {
ScannerNext scannerNext = (sc)->sc.hasNextInt();
ScannerValue scannerValue = (sc)-> sc.nextInt();
Number value = null;
while (value == null) {
if (scannerNext.hasNext(scanner)) {
value = scannerValue.getNext(scanner);
} else {
scanner.next();
}
}
return (Integer)value;
}
Ahora tienes que elegir lugares con una gran sección igual . Puede seleccionar una de esas secciones comenzando con Number value = null;
terminando con el }
antes de return ...
e invocar el método de extracción de refactorización automática de IDEs:
private Number readNumber(ScannerNext scannerNext, ScannerValue scannerValue) {
Number value = null;
while (value == null) {
if (scannerNext.hasNext(scanner)) {
value = scannerValue.getNext(scanner);
} else {
scanner.next();
}
}
return value;
}
private Integer readInteger() {
return (Integer) readNumber( (sc)->sc.hasNextInt(), (sc)-> sc.nextInt());
}
private Double readDouble() {
return (Double) readNumber( (sc)->sc.hasNextDouble(), (sc)-> sc.nextDouble());
}
Los comentarios argumentan contra el uso de interfaces personalizadas contra interfaces predefinidas desde la JVM.
Pero mi punto en esta respuesta fue cómo convertir código similar en código igual para que pueda extraerse a un único método en lugar de dar una solución concreta para este problema aleatorio.
La reflexión es una alternativa si no te importa el rendimiento.
private <T> T read(String type) throws Exception {
Method readNext = Scanner.class.getMethod("next" + type);
Method hasNext = Scanner.class.getMethod("hasNext" + type);
T value = null;
while (value == null) {
if ((Boolean) hasNext.invoke(scanner)) {
value = (T) readNext.invoke(scanner);
} else {
scanner.next();
}
}
return value;
}
Entonces llamas
Integer i = read("Int");
Mucha gente ha respondido que puede usar genéricos, pero también puede simplemente eliminar el método readInteger
y solo usar readDouble
, ya que los enteros se pueden convertir a dobles sin pérdida de datos.
No es una solución ideal, pero aún así logra la eliminación necesaria de código duplicado y tiene el beneficio adicional de no requerir Java-8.
// This could be done better.
static final Scanner scanner = new Scanner(System.in);
enum Read{
Int {
@Override
boolean hasNext() {
return scanner.hasNextInt();
}
@Override
<T> T next() {
return (T)Integer.valueOf(scanner.nextInt());
}
},
Dbl{
@Override
boolean hasNext() {
return scanner.hasNextDouble();
}
@Override
<T> T next() {
return (T)Double.valueOf(scanner.nextDouble());
}
};
abstract boolean hasNext();
abstract <T> T next();
// All share this method.
public <T> T read() {
T v = null;
while (v == null) {
if ( hasNext() ) {
v = next();
} else {
scanner.next();
}
}
return v;
}
}
public void test(String[] args) {
Integer i = Read.Int.read();
Double d = Read.Dbl.read();
}
Hay algunos problemas menores con esto, como el lanzamiento, pero debería ser una opción razonable.
Podrías tener algo con tres métodos:
- Uno que dice si hay un valor del tipo correcto
- Otro que obtiene el valor del tipo correcto.
- Otro que descarta cualquier token que tengas.
Por ejemplo:
interface Frobnitz<T> {
boolean has();
T get();
void discard();
}
Puedes pasar esto a tu método:
private <T> T read(Frobnitz<? extends T> frob) {
T value = null;
while (value == null) {
if (frob.has()) {
value = frob.get();
} else {
frob.discard();
}
}
return value;
}
Y luego solo implemente Frobnitz
para sus casos Double
y Integer
.
Para ser sincero, no estoy seguro de que esto te genere mucho, especialmente si solo tienes dos casos; Me inclinaría solo a absorber la pequeña cantidad de duplicaciones.
Un enfoque totalmente diferente de mi otra respuesta (y las otras respuestas): no use genéricos, sino que simplemente escriba los métodos de manera más concisa, para que realmente no note la duplicación.
TL; DR: reescribir los métodos como
while (!scanner.hasNextX()) scanner.next();
return scanner.nextX();
El objetivo general, escríbalo como un único método, solo es posible si acepta una cantidad de dinero adicional.
Las firmas de método Java no tienen en cuenta el tipo de devolución, por lo que no es posible hacer que un método next()
devuelva un Integer
en un contexto, y el Double
en otro (a menos que devuelva un supertipo común).
Como tal, debe tener algo en los sitios de llamadas para distinguir estos casos:
Puede considerar pasar algo como
Integer.class
oDouble.class
. Esto tiene la ventaja de que puede usar genéricos para saber que el valor devuelto coincide con ese tipo. Pero los llamantes podrían pasar algo más: ¿cómo manejaríasLong.class
, oString.class
? O necesita manejar todo, o falla en el tiempo de ejecución (no es una buena opción). Incluso con un límite más estricto (por ejemplo,Class<? extends Number>
), aún necesita manejar más queInteger
yDouble
.(Sin mencionar que escribir
Integer.class
yDouble.class
todas partes es muy detallado)Podrías considerar hacer algo como la respuesta de @ Ward (que me gusta, por cierto: si vas a hacerlo con genéricos, hazlo de esa manera), y pasar objetos funcionales que sean capaces de manejar el tipo de interés, además de proporcionar la información de tipo para indicar el tipo de devolución.
Pero, una vez más, debe pasar estos objetos funcionales en cada sitio de llamadas, que es realmente detallado.
Al tomar cualquiera de estos enfoques, puede agregar métodos auxiliares que pasen los parámetros apropiados al método de read
"genérico". Pero esto parece un paso atrás: en lugar de reducir el número de métodos a 1, se aumenta a 3.
Además, ahora tiene que distinguir estos métodos de ayuda de alguna manera en los sitios de llamadas, para poder llamar al apropiado:
Podría tener sobrecargas con un parámetro de tipo de valor , en lugar de tipo de clase, por ejemplo
Double read(Double d) Integer read(Integer d)
y luego llamar como
Double d = read(0.0); Integer i = read(0);
Double d = read(0.0); Integer i = read(0);
. Pero cualquiera que lea este código se preguntará qué es ese número mágico en el código: ¿hay algún significado para el0
?O, más fácil, simplemente llame a las dos sobrecargas algo diferente:
Double readDouble() Integer readInteger()
Esto es agradable y fácil: aunque es un poco más detallado que
read(0.0)
, es legible; y es mucho más conciso queread(Double.class)
.
Por lo tanto, esto nos ha devuelto a las firmas de métodos en el código de OP. Pero esto con suerte justifica por qué todavía desea mantener esos dos métodos. Ahora para abordar el contenido de los métodos:
Como Scanner.nextX()
no devuelve valores nulos, el método se puede volver a escribir como:
while (!scanner.hasNextX()) scanner.next();
return scanner.nextX();
Entonces, es muy fácil duplicar esto para los dos casos:
private Integer readInteger() {
while (!scanner.hasNextInt()) scanner.next();
return scanner.nextInt();
}
private Double readDouble() {
while (!scanner.hasNextDouble()) scanner.next();
return scanner.nextDouble();
}
Si lo desea, puede extraer el método dropUntil(Predicate<Scanner>)
para evitar duplicar el bucle, pero no estoy seguro de que realmente le ahorre tanto.
Una sola línea (casi) duplicada es mucho menos onerosa en su código que todos los genéricos y parámetros funcionales. Es simplemente código antiguo, que resulta ser más conciso (y, probablemente, más eficiente) que "nuevas" formas de escribirlo.
La otra ventaja de este enfoque es que no tiene que usar tipos encuadrados; puede hacer que los métodos retornen int
y double
, y que no tenga que pagar el impuesto de boxeo a menos que realmente lo necesite.
Esto puede no ser una ventaja para OP, ya que los métodos originales devuelven el tipo enmarcado; No sé si esto es genuinamente deseado, o simplemente un artefacto de la forma en que se escribió el ciclo. Sin embargo, es útil en general no crear esos objetos a menos que realmente los necesites.
Yo diría que use un método genérico, combinado con las interfaces funcionales introducidas en Java 8.
El método read
ahora se convierte en una función de orden superior .
private <T> T read(Predicate<Scanner> hasVal, Function<Scanner, T> nextVal) {
T value = null;
while (value == null) {
if (hasVal.test(scanner)) {
value = nextVal.apply(scanner);
} else {
scanner.next();
}
}
return value;
}
El código de llamada se convierte en:
read(Scanner::hasNextInt, Scanner::nextInt);
read(Scanner::hasNextDouble, Scanner::nextDouble);
read(Scanner::hasNextFloat, Scanner::nextFloat);
// ...
Por lo tanto, el método readInteger()
se puede adaptar de la siguiente manera:
private Integer readInteger() {
return read(Scanner::hasNextInt, Scanner::nextInt);
}