Traducción de conceptos POO a Java y C#: equivalencias y diferencias relevantes

Capítulo 10

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Mapa rápido: constructos equivalentes (y dónde no lo son)

Cuando “traducimos” POO entre Java y C#, lo importante no es copiar sintaxis, sino mapear el concepto a la construcción idiomática del lenguaje. A continuación tienes un mapa de equivalencias útiles y diferencias que impactan decisiones de diseño.

ConceptoJavaC#Diferencia relevante
ClaseclassclassEn C# hay más soporte de propiedades, records y patrones; en Java es común usar getters/setters explícitos o records.
InterfazinterfaceinterfaceAmbos soportan métodos default (Java) / implementaciones en interfaces (C#). Úsalos con moderación para no crear “mixins” confusos.
Clase abstractaabstract classabstract classEquivalentes; en C# es común combinar con propiedades abstractas.
Evitar herenciafinal classsealed classEquivalentes: bloquean herencia. Útil para invariantes fuertes o APIs cerradas.
Método no sobreescribiblefinalsealed overrideEn C# se sella típicamente un override concreto.
Propiedad / accesoGetters/setterspublic T X { get; private set; }En C# la propiedad es un miembro de primera clase; en Java el estándar sigue siendo métodos (salvo records).
Inmutabilidad “de fábrica”record (Java 16+)recordAmbos ofrecen records, pero su ecosistema/convenciones difieren (p. ej., con serialización, frameworks, etc.).
Visibilidadpublic/protected/private + package-privatepublic/protected/private/internalJava tiene visibilidad por paquete; C# tiene internal por ensamblado y combinaciones como protected internal.
ExcepcionesChecked/uncheckedNo checked exceptionsJava puede forzar declaración/captura; C# se apoya en documentación y convenciones.

Guía práctica: cómo traducir sin perder diseño

Paso 1: Identifica el “contrato” antes que la sintaxis

Antes de convertir código, escribe qué promete el tipo: qué operaciones expone, qué estados son válidos y qué errores comunica. Luego elige la construcción idiomática en cada lenguaje.

  • Si el contrato es “capacidad” (p. ej., IPaymentGateway): usa interfaz en ambos.
  • Si el contrato incluye estado compartido y comportamiento común: considera clase abstracta, pero revisa si composición es mejor.
  • Si el tipo no debe extenderse: marca final/sealed para proteger invariantes.

Paso 2: Encapsulación práctica: properties (C#) vs getters/setters (Java)

Trampa común: “como existe setter, lo expongo”. No. Expón operaciones, no campos mutables. En C#, las properties facilitan restringir escritura; en Java, lo equivalente suele ser omitir setters o hacerlos privados y ofrecer métodos con intención.

Intención de diseñoJava (idiomático)C# (idiomático)
Solo lectura públicagetX() sin setX()public T X { get; } o get; private set;
Mutación controladaMétodos con intención: renameTo(...)Métodos con intención + private set si aplica
Validación al cambiarValidación dentro del métodoValidación en método o en set privado

Ejemplo equivalente (evitando setter público):

// Java public final class User {     private String name;     public User(String name) {         this.name = requireNonBlank(name);     }     public String getName() { return name; }     public void renameTo(String newName) {         this.name = requireNonBlank(newName);     }     private static String requireNonBlank(String s) {         if (s == null || s.isBlank()) throw new IllegalArgumentException("name");         return s;     } }
// C# public sealed class User {     public string Name { get; private set; }     public User(string name) {         Name = RequireNonBlank(name);     }     public void RenameTo(string newName) {         Name = RequireNonBlank(newName);     }     private static string RequireNonBlank(string s) {         if (string.IsNullOrWhiteSpace(s)) throw new ArgumentException("name");         return s;     } }

Paso 3: abstract, sealed/final: decide por invariantes y extensión real

Trampa común: traducir “clase base” automáticamente. Si la extensión no es un caso de uso real, sella/finaliza el tipo y ofrece puntos de extensión mediante interfaces o composición.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

  • Java: final class para cerrar herencia; final en métodos para evitar overrides peligrosos.
  • C#: sealed class; y si necesitas permitir herencia pero bloquear un override específico: sealed override.

Ejemplo: permitir extensión controlada con plantilla (template method), sellando el flujo principal:

// Java public abstract class ReportGenerator {     public final byte[] generate() {         var data = fetchData();         var rendered = render(data);         return export(rendered);     }     protected abstract Object fetchData();     protected abstract Object render(Object data);     protected byte[] export(Object rendered) {         return rendered.toString().getBytes();     } }
// C# public abstract class ReportGenerator {     public byte[] Generate() {         var data = FetchData();         var rendered = Render(data);         return Export(rendered);     }     protected abstract object FetchData();     protected abstract object Render(object data);     protected virtual byte[] Export(object rendered) {         return System.Text.Encoding.UTF8.GetBytes(rendered.ToString()!);     } }

Nota: en C# el método principal no necesita ser sealed si no es virtual. En Java, marcarlo final comunica explícitamente “no override”.

Paso 4: Visibilidad: package-private (Java) vs internal (C#)

La visibilidad define fronteras de diseño. Al traducir, no te limites a “public por defecto”. Usa el nivel más restrictivo que permita el uso legítimo.

NecesidadJavaC#Uso típico
Solo dentro del tipoprivateprivateInvariantes y detalles internos.
Para subclasesprotectedprotectedPuntos de extensión (con cuidado).
Para el “módulo”/paquetepackage-private (sin modificador)internalColaboradores internos, factories, helpers.
API públicapublicpublicSuperficie estable y documentada.

Ejemplo: repositorio con implementación interna (misma idea, distinto mecanismo):

// Java (mismo paquete) interface UserRepository {     User findById(String id); } class SqlUserRepository implements UserRepository {     public User findById(String id) { /* ... */ return null; } }
// C# internal interface IUserRepository {     User FindById(string id); } internal sealed class SqlUserRepository : IUserRepository {     public User FindById(string id) { /* ... */ return null!; } }

Paso 5: Records e inmutabilidad: cuándo aplican y cómo no “romper” el modelo

Records son útiles para datos inmutables y transparentes (igualdad por valor). Trampa común: usarlos para entidades con identidad mutable o con invariantes complejas que requieren comportamiento rico.

EscenarioJavaC#Recomendación
DTO/resultado de consultarecordrecordBien: igualdad por valor, fácil de transportar.
Entidad con identidad (p. ej., Usuario en dominio)Clase normalClase normalEvita record si hay reglas/invariantes y mutación controlada.
Inmutabilidad con “copia con cambios”Constructor + métodos de copia (manual)with en recordsEn C# es muy natural; en Java puede ser verboso.

Ejemplo equivalente de “dato” con record:

// Java public record Money(String currency, long cents) {     public Money {         if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");     } }
// C# public readonly record struct Money(string Currency, long Cents) {     public Money(string currency, long cents) : this() {         if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("currency");         Currency = currency;         Cents = cents;     } }

Nota: en C# puedes elegir record (referencia) o record struct (valor). No lo traduzcas mecánicamente: decide según semántica (valor pequeño e inmutable suele encajar en struct).

Paso 6: Manejo de excepciones: checked (Java) vs no-checked (C#)

Trampa común: en Java, declarar checked exceptions “por si acaso”; en C#, ignorar por completo el contrato de error. En ambos, la regla práctica es: usa excepciones para condiciones excepcionales y comunica fallos esperables con resultados explícitos cuando tenga sentido (p. ej., TryX en C# o Optional en Java para ausencia).

SituaciónJavaC#Consejo de traducción
Error de validación de argumentosIllegalArgumentExceptionArgumentExceptionEquivalentes conceptuales.
Fallo de IOIOException (checked)IOExceptionEn Java debes decidir si propagar o envolver; en C# documenta y captura donde agregues contexto.
Ausencia de valorOptional<T>T? (nullable) o patrón TryNo conviertas ausencia en excepción salvo que sea realmente excepcional.

Ejemplo equivalente: agregar contexto al capturar y re-lanzar (sin perder la causa):

// Java public User loadUser(String id) {     try {         return gateway.fetch(id);     } catch (IOException ex) {         throw new RuntimeException("Failed to load user id=" + id, ex);     } }
// C# public User LoadUser(string id) {     try {         return gateway.Fetch(id);     } catch (IOException ex) {         throw new InvalidOperationException($"Failed to load user id={id}", ex);     } }

Trampas frecuentes al “traducir” sin criterio (y cómo evitarlas)

1) Exponer setters por costumbre

En C# es tentador dejar public set; porque “es lo normal con properties”. En Java, generar getters/setters automáticamente. En ambos casos, revisa si la mutación debe ser una operación con intención (método) y si el estado puede quedar inválido.

  • Preferir: RenameTo, ChangeAddress, AddItem.
  • Evitar: setStatus público si hay reglas de transición.

2) Traducir interfaces como clases abstractas (o al revés)

Si el objetivo es permitir múltiples implementaciones, interfaz suele ser el contrato más claro. Si necesitas compartir código, evalúa composición (un colaborador reutilizable) antes de una base abstracta.

3) Copiar patrones de visibilidad sin considerar el “módulo” real

Java package-private y C# internal no son idénticos, pero cumplen un rol similar: ocultar detalles dentro de un límite. Al migrar, define qué es “interno” (paquete/ensamblado) y ajusta accesos para no inflar el API público.

4) Usar records para todo

Records son excelentes para datos; no necesariamente para objetos con comportamiento rico, identidad o invariantes complejas. Si el tipo necesita métodos que preserven reglas, una clase con mutación controlada suele ser más clara.

Checklist de traducción (rápido y accionable)

  • ¿Qué miembros deben ser públicos de verdad? Reduce superficie: public solo para el contrato.
  • ¿Hay invariantes? Si sí, evita setters públicos; ofrece métodos con intención.
  • ¿Se permite extensión? Si no, usa final/sealed.
  • ¿Es “dato” o “comportamiento”? Considera record solo para datos.
  • ¿Cómo se comunica el fallo? Excepción con contexto para errores excepcionales; resultados explícitos para casos esperables.

Ahora responde el ejercicio sobre el contenido:

Al traducir un modelo POO entre Java y C#, ¿cuál enfoque es más adecuado para mantener un buen diseño al decidir entre interfaces, clases abstractas y tipos sellados/final?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La traducción correcta parte del contrato y no de la sintaxis. Se elige interfaz para capacidades, clase abstracta solo cuando realmente aporta comportamiento/estado común, y sealed/final para proteger invariantes cuando no se quiere extensión.

Portada de libro electrónico gratuitaPOO práctica y clara: clases, encapsulación, herencia y polimorfismo (con ejemplos en Java y C#)
100%

POO práctica y clara: clases, encapsulación, herencia y polimorfismo (con ejemplos en Java y C#)

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.