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.
| Concepto | Java | C# | Diferencia relevante |
|---|---|---|---|
| Clase | class | class | En C# hay más soporte de propiedades, records y patrones; en Java es común usar getters/setters explícitos o records. |
| Interfaz | interface | interface | Ambos soportan métodos default (Java) / implementaciones en interfaces (C#). Úsalos con moderación para no crear “mixins” confusos. |
| Clase abstracta | abstract class | abstract class | Equivalentes; en C# es común combinar con propiedades abstractas. |
| Evitar herencia | final class | sealed class | Equivalentes: bloquean herencia. Útil para invariantes fuertes o APIs cerradas. |
| Método no sobreescribible | final | sealed override | En C# se sella típicamente un override concreto. |
| Propiedad / acceso | Getters/setters | public 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+) | record | Ambos ofrecen records, pero su ecosistema/convenciones difieren (p. ej., con serialización, frameworks, etc.). |
| Visibilidad | public/protected/private + package-private | public/protected/private/internal | Java tiene visibilidad por paquete; C# tiene internal por ensamblado y combinaciones como protected internal. |
| Excepciones | Checked/unchecked | No checked exceptions | Java 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/sealedpara 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ño | Java (idiomático) | C# (idiomático) |
|---|---|---|
| Solo lectura pública | getX() sin setX() | public T X { get; } o get; private set; |
| Mutación controlada | Métodos con intención: renameTo(...) | Métodos con intención + private set si aplica |
| Validación al cambiar | Validación dentro del método | Validació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.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
- Java:
final classpara cerrar herencia;finalen 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.
| Necesidad | Java | C# | Uso típico |
|---|---|---|---|
| Solo dentro del tipo | private | private | Invariantes y detalles internos. |
| Para subclases | protected | protected | Puntos de extensión (con cuidado). |
| Para el “módulo”/paquete | package-private (sin modificador) | internal | Colaboradores internos, factories, helpers. |
| API pública | public | public | Superficie 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.
| Escenario | Java | C# | Recomendación |
|---|---|---|---|
| DTO/resultado de consulta | record | record | Bien: igualdad por valor, fácil de transportar. |
| Entidad con identidad (p. ej., Usuario en dominio) | Clase normal | Clase normal | Evita record si hay reglas/invariantes y mutación controlada. |
| Inmutabilidad con “copia con cambios” | Constructor + métodos de copia (manual) | with en records | En 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ón | Java | C# | Consejo de traducción |
|---|---|---|---|
| Error de validación de argumentos | IllegalArgumentException | ArgumentException | Equivalentes conceptuales. |
| Fallo de IO | IOException (checked) | IOException | En Java debes decidir si propagar o envolver; en C# documenta y captura donde agregues contexto. |
| Ausencia de valor | Optional<T> | T? (nullable) o patrón Try | No 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:
setStatuspú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:
publicsolo 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.