This article has multiple issues. Please help improve it or discuss these issues on the talk page. (Learn how and when to remove these messages)
|
En este artículo se comparan dos lenguajes de programación : C# y Java . Si bien el enfoque de este artículo se centra principalmente en los lenguajes y sus características, dicha comparación también tendrá en cuenta necesariamente algunas características de las plataformas y bibliotecas .
C# y Java son lenguajes similares que se tipan de forma estática, fuerte y manifiesta . Ambos están orientados a objetos y diseñados con semiinterpretación o compilación en tiempo de ejecución , y ambos son lenguajes entre llaves , como C y C++ .
Tipos de datos | Java | DO# |
---|---|---|
Decimales de tamaño arbitrario | Tipo de referencia; sin operadores [1] | Biblioteca de terceros [2] |
Números enteros de tamaño arbitrario | Tipo de referencia; sin operadores | Sí [3] |
Matrices | Sí [4] | Sí |
Tipo booleano | Sí | Sí |
Personaje | Sí [5] | Sí |
Números complejos | Biblioteca de terceros [6] | Sí |
Fecha/hora | Sí; tipo de referencia [7] | Sí; tipo de valor |
Tipos enumerados | Sí; tipo de referencia | Sí; escalar |
Número decimal de alta precisión | No; pero vea 'Decimales de tamaño arbitrario' más arriba | Tipo decimal de 128 bits (28 dígitos) [8] |
Número de punto flotante binario 32 IEEE 754 | Sí | Sí |
Número de punto flotante binario 64 IEEE 754 | Sí | Sí |
Tipos elevados (que aceptan valores nulos) | No; pero tipos de envoltura | Sí |
Punteros | No; [9] sólo referencias de métodos [10] | Sí [11] |
Tipos de referencia | Sí | Sí |
Enteros con signo | Sí; 8 , 16 , 32 , 64 bits | Sí; 8, 16, 32, 64 bits |
Instrumentos de cuerda | Tipo de referencia inmutable, Unicode | Tipo de referencia inmutable, Unicode |
Anotaciones de tipo | Sí | Sí |
Sistema de tipos de raíz única (unificado) | No; pero tipos de envoltura | Sí [12] |
Tuplas | No; disponibilidad limitada de terceros [13] | Sí [14] [15] |
Enteros sin signo | No, pero algunos métodos lo admiten [16] | Sí; 8, 16, 32, 64 bits |
Tipos de valores | No; solo tipos primitivos | Sí |
Ambos lenguajes están tipados estáticamente con orientación a objetos basada en clases. En Java, los tipos primitivos son especiales en el sentido de que no están orientados a objetos y no podrían haber sido definidos utilizando el lenguaje mismo. Tampoco comparten un ancestro común con los tipos de referencia. Todos los tipos de referencia de Java derivan de un tipo raíz común. C# tiene un sistema de tipos unificado en el que todos los tipos (excepto los punteros no seguros [17] ) derivan en última instancia de un tipo raíz común. En consecuencia, todos los tipos implementan los métodos de este tipo raíz, y los métodos de extensión definidos para el tipo de objeto se aplican a todos los tipos, incluso a los literales int primitivos y a los delegados . Esto permite que C#, a diferencia de Java, admita objetos con encapsulación que no sean tipos de referencia.
En Java, los tipos compuestos son sinónimos de tipos de referencia; no se pueden definir métodos para un tipo a menos que también sea un tipo de referencia de clase . En C#, los conceptos de encapsulación y métodos se han desvinculado del requisito de referencia, de modo que un tipo puede admitir métodos y encapsulación sin ser un tipo de referencia. Sin embargo, solo los tipos de referencia admiten métodos virtuales y especialización.
Ambos lenguajes admiten muchos tipos integrados que se copian y pasan por valor en lugar de por referencia. Java denomina a estos tipos tipos primitivos , mientras que en C# se denominan tipos simples . Los tipos primitivos/simples suelen tener compatibilidad nativa con la arquitectura del procesador subyacente.
Los tipos simples de C# implementan varias interfaces y, en consecuencia, ofrecen muchos métodos directamente en las instancias de los tipos, incluso en los literales. Los nombres de los tipos de C# también son meros alias de los tipos de Common Language Runtime (CLR). El System.Int64
tipo de C# es exactamente el mismo tipo que el tipo largo ; la única diferencia es que el primero es el nombre canónico de .NET , mientras que el segundo es un alias de C# para él.
Java no ofrece métodos directamente sobre tipos primitivos. En su lugar, los métodos que operan sobre valores primitivos se ofrecen a través de clases contenedoras primitivas complementarias . Existe un conjunto fijo de tales clases contenedoras, cada una de las cuales envuelve uno de los tipos primitivos del conjunto fijo. Como ejemplo, el tipo Java Long es un tipo de referencia que envuelve el tipo primitivo long . Sin embargo, no son el mismo tipo.
Tanto Java como C# admiten números enteros con signo con anchos de bits de 8, 16, 32 y 64 bits. Utilizan el mismo nombre o alias para los tipos, excepto para el número entero de 8 bits que se denomina byte en Java y sbyte (byte con signo) en C#.
C# admite tipos de enteros sin signo además de los tipos de enteros con signo . Los tipos sin signo son byte , ushort , uint y ulong para anchos de 8, 16, 32 y 64 bits, respectivamente. También se admiten operaciones aritméticas sin signo en los tipos. Por ejemplo, sumar dos enteros sin signo ( uint ) sigue dando como resultado un uint ; no un entero long o con signo.
Java no tiene tipos enteros sin signo. En particular, Java carece de un tipo primitivo para un byte sin signo . En cambio, el tipo byte de Java es sign extended , lo que es una fuente común de errores y confusión. [18]
Los enteros sin signo se excluyeron deliberadamente de Java porque James Gosling creía que los programadores no entenderían cómo funciona la aritmética sin signo.
En el diseño de lenguajes de programación, uno de los problemas habituales es que el lenguaje se vuelve tan complejo que nadie puede entenderlo. Uno de los pequeños experimentos que intenté fue preguntar a la gente sobre las reglas de la aritmética sin signo en C. Resulta que nadie entiende cómo funciona la aritmética sin signo en C. Hay algunas cosas obvias que la gente entiende, pero mucha gente no las entiende. [9] [19]
Las versiones 8 y 9 de Java agregaron algunas operaciones integradas limitadas con enteros sin signo, pero solo se exponen como métodos estáticos en las clases contenedoras primitivas; operan en tipos de enteros primitivos con signo, tratándolos como si no tuvieran signo. [20]
C# tiene un tipo y una notación literal para aritmética decimal de alta precisión (28 dígitos decimales) que es apropiada para cálculos financieros y monetarios. [21] [22] [23] A diferencia de los tipos de datos float y double , los números fraccionarios decimales como 0,1 se pueden representar exactamente en la representación decimal. En las representaciones float y double, dichos números a menudo tienen expansiones binarias no terminales, lo que hace que esas representaciones sean más propensas a errores de redondeo. [22]
Si bien Java no cuenta con un tipo incorporado, la biblioteca Java sí cuenta con un tipo decimal de precisión arbitraria . Este no se considera un tipo de lenguaje y no admite los operadores aritméticos habituales; en cambio, es un tipo de referencia que debe manipularse mediante los métodos de tipo. Vea más información sobre números de precisión y tamaño arbitrarios a continuación.
Ambos lenguajes ofrecen tipos aritméticos de precisión arbitraria definidos por biblioteca para cálculos con números enteros y puntos decimales de tamaño arbitrario.
Solo Java tiene un tipo de datos para cálculos de punto decimal de precisión arbitraria. Solo C# tiene un tipo para trabajar con números complejos .
En ambos lenguajes, la cantidad de operaciones que se pueden realizar en los tipos numéricos avanzados es limitada en comparación con los tipos de punto flotante IEEE 754 integrados . Por ejemplo, ninguno de los tipos de tamaño arbitrario admite la raíz cuadrada o los logaritmos .
C# permite que los tipos definidos por bibliotecas se integren con tipos y operadores existentes mediante el uso de conversiones implícitas/explícitas personalizadas y sobrecarga de operadores. Consulte el ejemplo en la sección Integración de tipos definidos por bibliotecas
Ambos lenguajes tienen un tipo de datos char (carácter) nativo como tipo simple. Aunque el tipo char se puede utilizar con operadores bit a bit, esto se realiza convirtiendo el valor char en un valor entero antes de la operación. Por lo tanto, el resultado de una operación bit a bit es un tipo numérico, no un carácter, en ambos lenguajes.
Ambos lenguajes tratan las cadenas como objetos ( inmutables ) de tipo de referencia. En ambos lenguajes, el tipo contiene varios métodos para manipular cadenas, analizar, formatear, etc. En ambos lenguajes, las expresiones regulares se consideran una característica externa y se implementan en clases separadas.
Las bibliotecas de ambos lenguajes definen clases para trabajar con fechas, horas, zonas horarias y calendarios en diferentes culturas. Java proporciona java.util.Date
, un tipo de referencia mutable con precisión de milisegundos, y (desde Java 8) el java.time
paquete (que incluye clases como LocalDate , LocalTime y LocalDateTime para valores de solo fecha, solo hora y fecha y hora), un conjunto de tipos de referencia inmutables con precisión de nanosegundos . [24] Por el contrario, C# es un tipo de valor de estructura inmutable para información de fecha y hora con precisión de 100 nanosegundos; la API de .NET 6 también agregó y , estructuras similares para operaciones de solo fecha o solo hora. [25] C# define además un tipo para trabajar con períodos de tiempo; Java 8 proporciona la clase para el mismo propósito. Ambos lenguajes admiten la aritmética de fecha y hora según diferentes culturas y zonas horarias.System.DateTime
System.DateOnly
System.TimeOnly
System.TimeSpan
java.time.Duration
C# permite al programador crear tipos de valor definidos por el usuario , utilizando la palabra clave struct . A diferencia de las clases y al igual que los primitivos estándar, estos tipos de valor se pasan y se asignan por valor en lugar de por referencia. También pueden ser parte de un objeto (ya sea como un campo o en un cuadro ) o almacenarse en una matriz sin la indirección de memoria que normalmente existe para los tipos de clase.
Debido a que los tipos de valor no tienen noción de un valor nulo y se pueden usar en matrices sin inicialización, siempre vienen con un constructor predeterminado implícito que esencialmente llena el espacio de memoria de la estructura con ceros. El programador solo puede definir constructores adicionales con uno o más argumentos. Los tipos de valor no tienen tablas de métodos virtuales y, debido a eso (y a la huella de memoria fija ), están sellados implícitamente. Sin embargo, los tipos de valor pueden (y con frecuencia lo hacen) implementar interfaces . Por ejemplo, los tipos enteros integrados implementan varias interfaces .
Aparte de los tipos primitivos incorporados, Java no incluye el concepto de tipos de valor.
Ambos lenguajes definen enumeraciones, pero se implementan de maneras fundamentalmente diferentes. Por ello, las enumeraciones son un área en la que fallan las herramientas diseñadas para traducir automáticamente código entre los dos lenguajes (como los conversores de Java a C#).
C# ha implementado enumeraciones de una manera similar a C, es decir, como envoltorios alrededor de los indicadores de bits implementados en tipos integrales primitivos (int, byte, short, etc.). Esto tiene beneficios de rendimiento y mejora la interacción con el código compilado de C/C++, pero proporciona menos funciones y puede provocar errores si los tipos de valor de bajo nivel se convierten directamente a un tipo de enumeración, como se permite en el lenguaje C#. Por lo tanto, se considera un azúcar sintáctico . [26] Por el contrario, Java implementa enumeraciones como una colección de instancias con todas las funciones, que requiere más memoria y no ayuda a la interacción con el código C/C++, pero proporciona funciones adicionales en la reflexión y el comportamiento intrínseco. La implementación en cada lenguaje se describe en la siguiente tabla.
Java | DO# | |
---|---|---|
Definición | En Java, el tipo de enumeración es una clase y sus valores son objetos (instancias) de esa clase. Los únicos valores válidos son los que figuran en la enumeración. El tipo de enumeración puede declarar campos , lo que permite que cada valor enumerado individual haga referencia a datos adicionales asociados de forma única con ese valor específico. El tipo de enumeración también puede declarar o anular métodos o implementar interfaces . [27] | Las enumeraciones en C# se derivan implícitamente del tipo Enum , que a su vez es un derivado del tipo de valor. El conjunto de valores de una enumeración de C# se define por el tipo subyacente , que puede ser un tipo entero con o sin signo de 8, 16, 32 o 64 bits. La definición de enumeración define los nombres de los valores enteros seleccionados. [27] [28] De manera predeterminada, al primer nombre se le asigna el valor 0 (cero) y los siguientes nombres se asignan en incrementos de 1. Cualquier valor del tipo primitivo subyacente es un valor válido del tipo de enumeración, aunque puede ser necesaria una conversión explícita para asignarlo. |
Combinatorio | Las colecciones de conjuntos de enumeraciones y mapas de Java proporcionan la funcionalidad de combinar múltiples valores de enumeración en un valor combinado. Estas colecciones especiales permiten la optimización del compilador para minimizar la sobrecarga que se genera al usar colecciones como mecanismo de combinación. | C# admite enumeraciones de mapa de bits en las que un valor real puede ser una combinación de valores enumerados unidos mediante OR bit a bit. Los métodos de formato y análisis definidos implícitamente por el tipo intentarán utilizar estos valores. |
Tanto en C# como en Java, los programadores pueden usar enumeraciones en una sentencia switch sin conversión a una cadena o a un tipo entero primitivo. Sin embargo, C# no permite el paso implícito a menos que la sentencia case no contenga ningún código, ya que es una causa común de errores difíciles de encontrar. [29] El paso implícito debe declararse explícitamente mediante una sentencia goto . [30]
C# implementa punteros de métodos orientados a objetos en forma de delegados . Un delegado es un tipo especial que puede capturar una referencia a un método. Esta referencia puede luego almacenarse en una variable de tipo delegado o pasarse a un método a través de un parámetro delegado para su posterior invocación. Los delegados de C# admiten covarianza y contravarianza , y pueden contener una referencia a cualquier método estático, método de instancia, método anónimo o expresión lambda compatible con la firma .
Los delegados no deben confundirse con los cierres y las funciones en línea. Los conceptos están relacionados porque una referencia a un cierre o función en línea debe estar capturada en una referencia de delegado para que sea útil. Pero un delegado no siempre hace referencia a una función en línea; también puede hacer referencia a métodos estáticos o de instancia existentes. Los delegados forman la base de los eventos de C#, pero tampoco deben confundirse con ellos.
Los delegados fueron deliberadamente excluidos de Java porque se los consideró innecesarios y perjudiciales para el lenguaje, y debido a posibles problemas de rendimiento. [31] En su lugar, se utilizan mecanismos alternativos. El patrón wrapper , que se asemeja a los delegados de C# en que permite al cliente acceder a uno o más métodos definidos por el cliente a través de una interfaz conocida , es uno de esos mecanismos. [ cita requerida ] Otro es el uso de objetos adaptadores que utilizan clases internas, que los diseñadores de Java argumentaron que son una mejor solución que las referencias de métodos enlazados. [31]
Consulte también ejemplos de delegados de C# y construcciones Java equivalentes.
C# permite que los tipos de valor/primitivos/simples se "eleven" para permitir el valor nulo especial además de los valores nativos del tipo. Un tipo se eleva agregando un sufijo al nombre del tipo; esto es equivalente a usar el tipo genérico , donde T es el tipo que se va a elevar. Las conversiones se definen implícitamente para convertir entre valores de la base y el tipo elevado. El tipo elevado se puede comparar con null o se puede probar para HasValue . Además, los operadores elevados se definen implícita y automáticamente en función de su base no elevada, donde, con la excepción de algunos operadores booleanos, un argumento nulo se propagará al resultado.?
Nullable<T>
Java no admite la elevación de tipos como concepto, pero todos los tipos primitivos integrados tienen tipos envolventes correspondientes, que sí admiten el valor nulo en virtud de ser tipos de referencia (clases).
Según la especificación de Java, cualquier intento de desreferenciar la referencia nula debe resultar en una excepción lanzada en tiempo de ejecución, específicamente una NullPointerException . (No tendría sentido desreferenciarla de otra manera, porque, por definición, no apunta a ningún objeto en la memoria). Esto también se aplica cuando se intenta desempaquetar una variable de un tipo contenedor, que evalúa a null : el programa lanzará una excepción, porque no hay ningún objeto para desempaquetar y, por lo tanto, ningún valor encajonado para participar en el cálculo posterior.
El siguiente ejemplo ilustra el comportamiento diferente. En C#, el operador lifting* propaga el valor nulo del operando; en Java, al desempaquetar la referencia nula se genera una excepción.
No todos los operadores de C# que se han eliminado se han definido para propagar valores nulos de forma incondicional si uno de los operandos es nulo . En concreto, los operadores booleanos se han eliminado para admitir la lógica ternaria, manteniendo así la impedancia con SQL .
Los operadores booleanos de Java no admiten lógica ternaria ni están implementados en la biblioteca de clases base.
C# cuenta con un tipo dinámico de enlace tardío que admite la invocación dinámica sin reflexión, la interoperabilidad con lenguajes dinámicos y el enlace ad hoc a (por ejemplo) modelos de objetos de documentos. El tipo dinámico resuelve el acceso a los miembros de forma dinámica en tiempo de ejecución, en lugar de hacerlo de forma estática o virtual en tiempo de compilación. El mecanismo de búsqueda de miembros es extensible con la reflexión tradicional como mecanismo de respaldo.
Hay varios casos de uso para el tipo dinámico en C#:
Java no admite un tipo de enlace tardío. Los casos de uso para el tipo dinámico de C# tienen diferentes construcciones correspondientes en Java:
Véase también el ejemplo #Interoperabilidad con lenguajes dinámicos.
Java excluye los punteros y la aritmética de punteros dentro del entorno de ejecución de Java. Los diseñadores del lenguaje Java razonaron que los punteros son una de las principales características que permiten a los programadores introducir errores en su código y decidieron no admitirlos. [9] Java no permite pasar y recibir directamente objetos/estructuras hacia/desde el sistema operativo subyacente y, por lo tanto, no necesita modelar objetos/estructuras para un diseño de memoria tan específico, diseños que con frecuencia implicarían punteros. La comunicación de Java con el sistema operativo subyacente se basa en cambio en la Interfaz nativa de Java (JNI), donde la comunicación con/adaptación a un sistema operativo subyacente se maneja a través de una capa de unión externa .
Aunque C# permite el uso de punteros y la aritmética de punteros correspondiente, los diseñadores del lenguaje C# tenían las mismas preocupaciones de que los punteros podrían usarse potencialmente para eludir las estrictas reglas de acceso a objetos. Por lo tanto, C# también excluye los punteros por defecto. [33] Sin embargo, debido a que los punteros son necesarios cuando se invocan muchas funciones nativas, se permiten punteros en un modo inseguro explícito . Los bloques de código o métodos que usan los punteros deben estar marcados con la palabra clave inseguro para poder usar punteros, y el compilador requiere el modificador para permitir la compilación de dicho código. Los ensamblajes que se compilan utilizando el modificador se marcan como tales y solo se pueden ejecutar si se confía explícitamente en ellos. Esto permite usar punteros y aritmética de punteros para pasar y recibir objetos directamente hacia/desde el sistema operativo u otras API nativas utilizando el diseño de memoria nativo para esos objetos, al mismo tiempo que se aísla dicho código potencialmente inseguro en ensamblajes específicamente confiables./unsafe
/unsafe
En ambos lenguajes las referencias son un concepto central. Todas las instancias de clases son por referencia .
Si bien no es directamente evidente en la sintaxis del lenguaje en sí , ambos lenguajes admiten el concepto de referencias débiles . Una instancia a la que solo se hace referencia mediante referencias débiles es elegible para la recolección de elementos no utilizados como si no hubiera ninguna referencia. En ambos lenguajes, esta característica se expone a través de las bibliotecas asociadas, aunque en realidad es una característica central del entorno de ejecución.
Además de las referencias débiles, Java tiene referencias suaves . Son muy similares a las referencias débiles, pero la máquina virtual Java (JVM) no desasignará los objetos referenciados de manera suave hasta que se necesite la memoria.
Tipos de referencia | Java | DO# |
---|---|---|
Recolección de basura | Sí | Sí |
Referencias débiles | Sí | Sí |
Cola de referencia (interacción con la recolección de basura) | Sí | Sí |
Referencias suaves | Sí | No |
Referencias fantasma | Sí | No |
Soporte de proxy | Sí; generación de proxy | Sí; contextos de objetos |
Las matrices y las colecciones son conceptos presentes en ambos lenguajes.
Matrices y colecciones | Java | DO# |
---|---|---|
Tipos de datos abstractos | Sí | Sí |
Matrices de índice unidimensionales basadas en cero | Sí | Sí |
Matrices multidimensionales, rectangulares (matriz única) | No | Sí |
Matrices multidimensionales, dentadas (matrices de matrices) | Sí | Sí |
Matrices no basadas en cero | No | Alguno |
Matrices y colecciones unificadas | No | Sí |
Mapas/diccionarios | Sí | Sí |
Diccionarios ordenados | Sí | Sí [34] |
Conjuntos | Sí | Sí |
Conjuntos ordenados | Sí | Sí [35] |
Listas/vectores | Sí | Sí |
Colas/pilas | Sí | Sí |
Cola de prioridad | Sí | Sí [36] |
Bolsas/multisets | Biblioteca de terceros | Sí |
Colecciones optimizadas para concurrencia | Sí | Sí [37] |
La sintaxis utilizada para declarar y acceder a matrices es idéntica, excepto que C# ha agregado sintaxis para declarar y manipular matrices multidimensionales.
Java | DO# |
---|---|
Los arrays son especializaciones implícitamente directas de Object . No están unificados con los tipos de colección. | Las matrices en C# son especializaciones implícitas de la System.Array clase que implementa varias interfaces de colección . |
Las matrices y las colecciones están completamente separadas y no están unificadas. No se pueden pasar matrices donde se esperan secuencias o colecciones (aunque se pueden encapsular usando Arrays.asList ). | Se pueden pasar matrices donde se esperan secuencias ( IEnumerable ) o interfaces de colecciones/listas . Sin embargo, las operaciones de colección que alteran la cantidad de elementos (insertar/agregar/eliminar) generarán excepciones, ya que estas operaciones no son compatibles con las matrices. |
La instrucción for acepta matrices o Iterable s. Todas las colecciones implementan Iterable . Esto significa que se puede utilizar la misma sintaxis corta en bucles for . | La declaración foreach itera a través de una secuencia utilizando una implementación específica del método GetEnumerator , generalmente implementado a través de la interfaz IEnumerable o . [38] Debido a que las matrices siempre implementan implícitamente estas interfaces , el bucle también iterará a través de las matrices.IEnumerable<T> |
En ambos lenguajes, las matrices de tipos de referencia son covariantes. Esto significa que una String[] matriz es asignable a variables de Object[] , ya que String es una especialización de (asignable a) Object . En ambos lenguajes, las matrices realizarán una verificación de tipo al insertar nuevos valores, ya que de lo contrario se comprometería la seguridad de tipo. Esto contrasta con la forma en que se han implementado las colecciones genéricas en ambos lenguajes. | |
No son matrices multidimensionales (matrices rectangulares), sino matrices de referencias a matrices ( matrices irregulares ). | Matrices multidimensionales (matrices rectangulares) y matrices de referencias a matrices ( matrices irregulares ). |
Las matrices no se pueden redimensionar (aunque el uso del System.arraycopy() método puede permitir redimensionar matrices en varios pasos) | Es posible cambiar el tamaño de las matrices conservando los valores existentes utilizando el Array.Resize() método de matriz estática (pero esto puede devolver una nueva matriz). |
Implementado como una actualización para la java.util biblioteca que tiene características adicionales, como estructuras de datos como conjuntos y conjuntos vinculados, y tiene varios algoritmos para manipular elementos de una colección, como encontrar el elemento más grande basado en algún Comparator<T> objeto, encontrar el elemento más pequeño, encontrar sublistas dentro de una lista, invertir el contenido de una lista, barajar el contenido de una lista, crear versiones inmutables de una colección, realizar ordenamientos y hacer búsquedas binarias. [39] | El marco de colecciones de C# consta de clases de los espacios de nombres System.Collections y con varias interfacesSystem.Collections.Generic útiles , clases abstractas y estructuras de datos. [40] NET 3.5 agregó un espacio de nombres que contiene varios métodos de extensión para consultar colecciones, como Aggregate , All , Average , Distinct , Join , Union y muchos otros. Las consultas que utilizan estos métodos se denominan consultas integradas del lenguaje (LINQ).System.Linq |
En algunos casos, las matrices multidimensionales pueden aumentar el rendimiento debido a una mayor localidad (ya que hay una desreferencia de puntero en lugar de una para cada dimensión de la matriz, como es el caso de las matrices escalonadas). Sin embargo, dado que todo acceso a los elementos de la matriz en una matriz multidimensional requiere multiplicación/desplazamiento entre las dos o más dimensiones, esto es una ventaja solo en escenarios de acceso muy aleatorio.
Otra diferencia es que se puede asignar toda la matriz multidimensional con una sola aplicación del operador new , mientras que las matrices irregulares requieren bucles y asignaciones para cada dimensión. Sin embargo, Java proporciona una construcción sintáctica para asignar una matriz irregular con longitudes regulares; los bucles y las asignaciones múltiples las realiza la máquina virtual y no necesitan ser explícitos en el nivel de origen.
Ambos lenguajes cuentan con un amplio conjunto de tipos de colecciones que incluye varios tipos de listas ordenadas y desordenadas, mapas/diccionarios, conjuntos, etc.
C# ofrece dos métodos diferentes para crear tipos de tuplas (también conocidos como tipos de producto ). El primero es a través de las System.Tuple
clases, que son tipos de referencia inmutables proporcionados por la API del marco (a partir de .NET Framework 4.0) para crear tipos de tuplas genéricos. [14] Este primer método ha sido reemplazado desde entonces por el segundo, las System.ValueTuple
estructuras, que son tipos de valores mutables proporcionados por la API del marco (a partir de .NET Framework 4.7). [15]
Aunque los dos métodos parecen superficialmente similares, tienen múltiples diferencias notables. Los tipos ValueTuple son tipos de valor, por lo que tienen una huella de memoria más compacta; además, los tipos ValueTuple exponen sus contenidos como campos mutables, en comparación con las propiedades inmutables de las clases Tuple . Finalmente, desde la versión 7.0 de C#, el lenguaje tiene soporte sintáctico nativo para la construcción, deconstrucción y manipulación de tuplas como instancias ValueTuple ; esto también permite el cambio de nombre arbitrario de los campos constituyentes de las tuplas (a diferencia de Tuple , donde los campos siempre se denominan Item1 , Item2 , etc.).
Java no proporciona tipos de tuplas como parte de su lenguaje o API estándar; existen numerosas bibliotecas de terceros que pueden proporcionar tipos de tuplas, [41] pero todas son necesariamente similares a las System.Tuple
clases de C#. En comparación con los tipos ValueTuple de C# y su sintaxis asociada, son más difíciles de usar (requieren el uso explícito de constructores o métodos de fábrica estáticos para crearlos, requieren acceso de miembros individuales para deconstruirlos y tienen nombres fijos para sus elementos).
Expresiones y operadores | Java | DO# |
---|---|---|
Operadores aritméticos | Sí | Sí |
Operadores lógicos | Sí | Sí |
Operadores lógicos bit a bit | Sí | Sí |
Condicional | Sí | Sí |
Concatenación de cadenas | Sí | Sí |
Interpolación de cadenas | Sí; Desde Java 21 ( detalles ) | Sí [42] |
Cadenas textuales (aquí) | Sí [43] | Sí [44] |
Reparto | Sí | Sí |
Boxeo | Sí; implícito | Sí; implícito |
Desempaquetado | Sí; implícito | Sí; explícito |
Operadores elevados | No; Ver java.util.Optional | Sí |
Control de desbordamiento | No | Sí |
Evaluación estricta de punto flotante | Sí; opt-in/out (inclusión/exclusión voluntaria) | Sí; opt-in [45] |
Ambos lenguajes permiten el boxing y unboxing automático , es decir, permiten la conversión implícita entre cualquier tipo primitivo y los tipos de referencia correspondientes.
En C#, los tipos primitivos son subtipos del tipo Object. En Java esto no es así; cualquier tipo primitivo dado y el tipo contenedor correspondiente no tienen una relación específica entre sí, excepto por el autoboxing y el unboxing, que actúan como azúcar sintáctico para intercambiar entre ellos. Esto se hizo intencionalmente, para mantener la compatibilidad con versiones anteriores de Java, en las que no se permitía la conversión automática y el programador trabajaba con dos conjuntos separados de tipos: los tipos primitivos y la jerarquía de tipos contenedor (de referencia). [46]
Esta diferencia tiene las siguientes consecuencias. En primer lugar, en C#, los tipos primitivos pueden definir métodos, como una anulación del ToString()
método de Object. En Java, esta tarea la llevan a cabo las clases contenedoras primitivas .
En segundo lugar, en Java se necesita una conversión adicional cada vez que se intenta desreferenciar directamente un valor primitivo, ya que no se enmarcará automáticamente. La expresión convertirá un literal entero en una cadena en Java mientras que realiza la misma operación en C#. Esto se debe a que la última es una llamada de instancia en el valor primitivo 42 , mientras que la primera es una llamada de instancia en un objeto de tipo .((Integer)42).toString()
42.ToString()
java.lang.Integer
Finalmente, otra diferencia es que Java hace un uso intensivo de tipos en caja en genéricos (ver más abajo).
Declaraciones | Java | DO# |
---|---|---|
Bucles | Sí | Sí |
Condicionales | Sí | Sí |
Control de flujo | Sí | Sí |
Asignación | Sí | Sí |
Control de excepciones | Sí | Sí |
Declaración de variable | Sí | Sí |
Inferencia de tipo variable | Sí [47] | Sí |
Eliminación determinista (bloques ARM) | Sí | Sí |
Ambos lenguajes se consideran lenguajes de "llaves" en la familia C/C++. En general, las sintaxis de los lenguajes son muy similares. La sintaxis a nivel de declaración y expresión es casi idéntica con una inspiración obvia de la tradición C/C++. A nivel de definición de tipos (clases e interfaces ) existen algunas diferencias menores. Java es explícito en cuanto a la extensión de clases e implementación de interfaces , mientras que C# lo infiere a partir del tipo de tipos del que deriva una nueva clase/ interfaz .
C# admite más funciones que Java, lo que hasta cierto punto también es evidente en la sintaxis que especifica más palabras clave y más reglas gramaticales que Java.
A medida que los lenguajes evolucionaron, los diseñadores de ambos lenguajes se enfrentaron a situaciones en las que querían ampliar los lenguajes con nuevas palabras clave o sintaxis. Las nuevas palabras clave, en particular, pueden romper el código existente en el nivel de fuente, es decir, el código antiguo puede dejar de compilarse si se presenta a un compilador para una versión posterior del lenguaje. Los diseñadores de lenguajes están interesados en evitar tales regresiones. Los diseñadores de los dos lenguajes han seguido caminos diferentes al abordar este problema.
Los diseñadores del lenguaje Java han evitado las palabras clave nuevas tanto como han podido, prefiriendo en su lugar introducir nuevas construcciones sintácticas que antes no eran legales o reutilizar palabras clave existentes en nuevos contextos. De esta manera no ponían en peligro la compatibilidad con versiones anteriores. Un ejemplo de lo primero se puede encontrar en cómo se extendió el bucle for para aceptar tipos iterables. Un ejemplo de lo segundo se puede encontrar en cómo se reutilizaron las palabras clave extends y (especialmente) super para especificar límites de tipo cuando se introdujeron los genéricos en Java 1.5. En un momento (Java 1.4) se introdujo una nueva palabra clave assert que antes no estaba reservada como palabra clave. Esto tenía el potencial de invalidar código que antes era válido, si, por ejemplo, el código utilizaba assert como identificador. Los diseñadores decidieron abordar este problema con una solución de cuatro pasos: 1) Introducir un cambio de compilador que indique si se debe utilizar Java 1.4 o posterior, 2) Marcar assert como palabra clave solo al compilar como Java 1.4 y posterior, 3) Establecer como predeterminado 1.3 para evitar que el código anterior (que no es compatible con 1.4) no sea válido y 4) Emitir advertencias, si la palabra clave se utiliza en modo Java 1.3, para permitir cambios en el código.
Los diseñadores del lenguaje C# han introducido varias palabras clave nuevas desde la primera versión. Sin embargo, en lugar de definir estas palabras clave como palabras clave globales , las definen como palabras clave sensibles al contexto . Esto significa que incluso cuando introdujeron (entre otras) las palabras clave partial y yield en C# 2.0, el uso de esas palabras como identificadores sigue siendo válido ya que no hay conflicto posible entre el uso como palabra clave y el uso como identificador, dado el contexto. Por lo tanto, la sintaxis actual de C# es totalmente compatible con el código fuente escrito para cualquier versión anterior sin especificar la versión del lenguaje que se utilizará.
palabra clave | Característica, ejemplo de uso |
---|---|
marcado , desmarcado | En C#, los bloques de declaraciones o expresiones verificadas pueden habilitar la verificación en tiempo de ejecución para detectar desbordamientos aritméticos . [48] |
obtener , establecer | C# implementa propiedades como parte de la sintaxis del lenguaje con sus correspondientes accesores get y set opcionales , como una alternativa a los métodos de acceso utilizados en Java, lo que no es una característica del lenguaje sino un patrón de codificación basado en convenciones de nombres de métodos. |
ir a | C# admite la goto palabra clave. Esto puede ser útil en ocasiones, por ejemplo, para implementar máquinas de estados finitos o para código generado , pero generalmente se recomienda el uso de un método más estructurado de flujo de control (consulte las críticas a la declaración goto ). Java no admite la declaración goto (pero goto es una palabra reservada). Sin embargo, Java admite las declaraciones break y continue etiquetadas , que en ciertas situaciones se pueden usar cuando, de lo contrario, se podría usar una declaración goto .switch ( color ) { case Color . Blue : Console . WriteLine ( "El color es azul" ); break ; case Color . DarkBlue : Console . WriteLine ( "El color es oscuro" ); goto case Color . Blue ; // ... } |
cerrar | En C#, la palabra clave lock es una abreviatura para sincronizar el acceso a un bloque de código entre subprocesos (usando un Monitor ), envuelto en un bloque try ... finally . |
fuera , ref | C# admite parámetros de salida y de referencia . Estos permiten devolver múltiples valores de salida desde un método o pasar valores por referencia. |
estrictofp | Java utiliza strictfp para garantizar que los resultados de las operaciones de punto flotante permanezcan iguales en todas las plataformas. |
cambiar | En C#, la sentencia switch también opera en cadenas y long. Se permite el paso a través de sentencias vacías y es posible a través de 'goto case' para sentencias que contienen código. La sentencia switch de Java opera en cadenas (desde Java 7 ) pero no en el tipo primitivo long , y pasa a través de todas las sentencias (excluyendo aquellas con ' break '). [49] |
sincronizado | En Java, la palabra clave sincronizada es una abreviatura para sincronizar el acceso a un bloque de código entre subprocesos (usando un Monitor ), envuelto en un bloque try ... finally . |
lanza | Java requiere que cada método declare las excepciones controladas o las superclases de las excepciones controladas que puede generar. Cualquier método también puede declarar opcionalmente la excepción no controlada que genera. C# no tiene esa sintaxis.public int readItem ( ) lanza java.io.IOException { // ... } |
usando | En C#, el uso hace que el método Dispose (implementado a través de la interfaz IDisposable ) del objeto declarado se ejecute después de que se haya ejecutado el bloque de código o cuando se lanza una excepción dentro del bloque de código. // Crea un archivo pequeño "test.txt", escribe una cadena, // ... y ciérralo (incluso si ocurre una excepción) usando ( var file = new StreamWriter ( " test.txt" )) { file.write ( "test" ) ; } En Java SE 7 se ha añadido una construcción similar [50] denominada try-with-resources: intentar ( BufferedReader br = new BufferedReader ( new FileReader ( ruta ))) { return br . readLine (); } |
Tanto C# como Java están diseñados desde cero como lenguajes orientados a objetos que utilizan un envío dinámico , con una sintaxis similar a C++ (C++ a su vez deriva de C ). Sin embargo, ninguno de los dos lenguajes es un superconjunto de C o C++.
Orientación a objetos | Java | DO# |
---|---|---|
Clases | obligatorio | obligatorio |
Interfaces | Sí | Sí |
Clases abstractas | Sí | Sí |
Niveles de accesibilidad de los miembros | público, paquete, protegido, privado | público, interno, protegido, privado, interno protegido |
Clases internas a nivel de clase | Sí | Sí |
Clases internas a nivel de instancia | Sí | No |
Clases anónimas a nivel de declaración (local) | Sí | Sí; pero sin métodos |
Clases parciales | No; Biblioteca de terceros [51] | Sí |
Clases anónimas implícitas (inferidas) | No; No es necesario | Sí [52] |
Depreciación /obsolescencia | Sí | Sí |
Control de versiones por sobrecarga | Alguno | Sí |
Las enumeraciones pueden implementar interfaces | Sí | No |
Propiedades | No | Sí |
Eventos | Proporcionado por bibliotecas estándar | Función de idioma incorporada |
Sobrecarga del operador | No | Sí |
Indexadores | No | Sí |
Conversiones implícitas | No; pero vea autoboxing | Sí |
Conversiones explícitas | Sí | Sí |
C# permite dividir una definición de clase en varios archivos fuente mediante una característica llamada clases parciales . Cada parte debe estar marcada con la palabra clave partial . Todas las partes deben presentarse al compilador como parte de una única compilación. Las partes pueden hacer referencia a miembros de otras partes. Las partes pueden implementar interfaces y una parte puede definir una clase base. La característica es útil en escenarios de generación de código (como el diseño de interfaz de usuario (UI)), donde un generador de código puede proporcionar una parte y el desarrollador otra parte para compilarlas juntas. De este modo, el desarrollador puede editar su parte sin el riesgo de que un generador de código sobrescriba ese código en algún momento posterior. A diferencia del mecanismo de extensión de clase, una clase parcial permite dependencias circulares entre sus partes, ya que se garantiza que se resolverán en tiempo de compilación. Java no tiene un concepto correspondiente.
Ambos lenguajes permiten clases internas , en las que una clase se define léxicamente dentro de otra clase. Sin embargo, en cada lenguaje estas clases internas tienen una semántica bastante diferente.
En Java, a menos que la clase interna se declare como estática , una referencia a una instancia de una clase interna lleva consigo una referencia a la clase externa. Como resultado, el código en la clase interna tiene acceso tanto a los miembros estáticos como a los no estáticos de la clase externa. Para crear una instancia de una clase interna no estática, se debe nombrar la instancia de la clase externa que la abarca. [53] Esto se hace a través de un nuevo operador new introducido en JDK 1.3: . Esto se puede hacer en cualquier clase que tenga una referencia a una instancia de la clase externa.outerClassInstance.new Outer.InnerClass()
En C#, una clase interna es conceptualmente lo mismo que una clase normal. En cierto sentido, la clase externa solo actúa como un espacio de nombres. Por lo tanto, el código de la clase interna no puede acceder a miembros no estáticos de la clase externa a menos que lo haga a través de una referencia explícita a una instancia de la clase externa. Los programadores pueden declarar la clase interna como privada para permitir que solo la clase externa tenga acceso a ella.
Java proporciona otra característica llamada clases locales o clases anónimas , que se pueden definir dentro del cuerpo de un método. Por lo general, se utilizan para implementar una interfaz con solo uno o dos métodos, que suelen ser controladores de eventos. Sin embargo, también se pueden utilizar para anular los métodos virtuales de una superclase. Los métodos de esas clases locales tienen acceso a las variables locales del método externo declaradas como final . C# satisface los casos de uso para estas proporcionando delegados anónimos ; consulte el manejo de eventos para obtener más información sobre esto.
C# también proporciona una característica llamada tipos/clases anónimos , pero es bastante diferente del concepto de Java con el mismo nombre. Permite al programador crear una instancia de una clase proporcionando solo un conjunto de nombres para las propiedades que debe tener la clase y una expresión para inicializar cada una. Los tipos de las propiedades se infieren de los tipos de esas expresiones. Estas clases declaradas implícitamente se derivan directamente de object .
Los delegados de multidifusión de C# se utilizan con eventos . Los eventos brindan soporte para la programación basada en eventos y son una implementación del patrón de observador . Para respaldar esto, existe una sintaxis específica para definir eventos en clases y operadores para registrar, anular el registro o combinar controladores de eventos.
Consulte aquí para obtener información sobre cómo se implementan los eventos en Java.
La sobrecarga de operadores y las conversiones definidas por el usuario son características independientes que tienen como objetivo permitir que los nuevos tipos se conviertan en ciudadanos de primera clase en el sistema de tipos. Al utilizar estas características en C#, se han integrado tipos como Complex y decimal para que los operadores habituales como la suma y la multiplicación funcionen con los nuevos tipos. A diferencia de C++, C# restringe el uso de la sobrecarga de operadores, prohibiéndola para los operadores new , , , , , y cualquier variación de declaraciones compuestas como . Pero los operadores compuestos llamarán a operadores simples sobrecargados, como llamar a y . [54]( )
||
&&
=
+=
-=
-
=
Java no incluye sobrecarga de operadores ni conversiones personalizadas para evitar el abuso de la función y mantener el lenguaje simple. [55]
C# también incluye indexadores que pueden considerarse un caso especial de sobrecarga de operadores (como C++ operator[]
) o propiedades get / set parametrizadas . Un indexador es una propiedad que utiliza uno o más parámetros (índices); los índices pueden ser objetos de cualquier tipo:this[]
myList [ 4 ] = 5 ; cadena nombre = xmlNode . Atributos [ "nombre" ]; pedidos = customerMap [ elCliente ];
Java no incluye indexadores. El patrón común de Java implica escribir métodos de obtención y establecimiento explícitos, mientras que un programador de C# utilizaría un indexador.
Campos e inicialización | Java | DO# |
---|---|---|
Campos | Sí | Sí |
Constantes | Sí | Sí, pero no admite el paso de parámetros constantes [56] |
Constructores estáticos (de clase) | Sí | Sí |
Constructores de instancias | Sí | Sí |
Finalizadores/destructores | Sí | Sí |
Inicializadores de instancia | Sí | No; se puede simular con el constructor de instancias |
Inicialización de objetos | De abajo hacia arriba (campos y constructores) | De arriba hacia abajo (campos); de abajo hacia arriba (constructores) |
Inicializadores de objetos | Sí | Sí |
Inicializadores de colección | Métodos de varargs estáticos | Sí |
Inicializadores de matriz | Sí | Sí |
Tanto en C# como en Java, los campos de un objeto se pueden inicializar mediante inicializadores de variables (expresiones que se pueden asignar a las variables donde están definidas) o mediante constructores (subrutinas especiales que se ejecutan cuando se crea un objeto). Además, Java contiene inicializadores de instancia , que son bloques de código anónimos sin argumentos que se ejecutan después de la llamada explícita (o implícita) al constructor de una superclase, pero antes de que se ejecute el constructor.
C# inicializa los campos de objeto en el siguiente orden al crear un objeto:
Algunos de los campos anteriores pueden no ser aplicables (por ejemplo, si un objeto no tiene campos estáticos ). Los campos derivados son aquellos que están definidos en la clase directa del objeto, mientras que el campo base es un término para los campos que están definidos en una de las superclases del objeto. Tenga en cuenta que una representación de objeto en la memoria contiene todos los campos definidos en su clase o cualquiera de sus superclases, incluso si algunos campos en las superclases están definidos como privados.
Se garantiza que cualquier inicializador de campo surta efecto antes de que se llame a cualquier constructor, ya que tanto el constructor de instancia de la clase del objeto como sus superclases se llaman después de que se llame a los inicializadores de campo. Sin embargo, existe una trampa potencial en la inicialización de objetos cuando se llama a un método virtual desde un constructor base. El método anulado en una subclase puede hacer referencia a un campo que está definido en la subclase, pero es posible que este campo no se haya inicializado porque el constructor de la subclase que contiene la inicialización de campo se llama después del constructor de su clase base.
En Java, el orden de inicialización es el siguiente:
Al igual que en C#, un nuevo objeto se crea llamando a un constructor específico. Dentro de un constructor, la primera instrucción puede ser una invocación de otro constructor. Si esto se omite, el compilador agrega implícitamente la llamada al constructor sin argumentos de la superclase. De lo contrario, se puede llamar explícitamente a otro constructor sobrecargado de la clase del objeto o a un constructor de la superclase. En el primer caso, el constructor llamado volverá a llamar a otro constructor (ya sea de la clase del objeto o de su subclase) y la cadena, tarde o temprano, termina en la llamada a uno de los constructores de la superclase.
Después de llamar a otro constructor (que provoca la invocación directa del constructor de la superclase, y así sucesivamente, hasta llegar a la clase Object), se inicializan las variables de instancia definidas en la clase del objeto. Incluso si no hay inicializadores de variables definidos explícitamente para algunas variables, estas variables se inicializan con valores predeterminados. Tenga en cuenta que las variables de instancia definidas en las superclases ya están inicializadas en este punto, porque fueron inicializadas por un constructor de la superclase cuando se lo llamó (ya sea por el código del constructor o por inicializadores de variables realizados antes del código del constructor o implícitamente con valores predeterminados). En Java, los inicializadores de variables se ejecutan de acuerdo con su orden textual en el archivo fuente.
Por último, se ejecuta el cuerpo del constructor. Esto garantiza el orden correcto de inicialización, es decir, los campos de una clase base terminan la inicialización antes de que comience la inicialización de los campos de una clase de objeto.
Existen dos posibles trampas principales en la inicialización de objetos de Java. En primer lugar, los inicializadores de variables son expresiones que pueden contener llamadas a métodos. Dado que los métodos pueden hacer referencia a cualquier variable definida en la clase, el método llamado en un inicializador de variable puede hacer referencia a una variable que esté definida debajo de la variable que se está inicializando. Dado que el orden de inicialización corresponde al orden textual de las definiciones de variables, dicha variable no se inicializaría con el valor prescrito por su inicializador y contendría el valor predeterminado. Otra trampa potencial es cuando un método que se reemplaza en la clase derivada se llama en el constructor de la clase base, lo que puede llevar a un comportamiento que el programador no esperaría cuando se crea un objeto de la clase derivada. De acuerdo con el orden de inicialización, el cuerpo del constructor de la clase base se ejecuta antes de que se evalúen los inicializadores de variables y antes de que se ejecute el cuerpo del constructor de la clase derivada. Sin embargo, el método reemplazado llamado desde el constructor de la clase base puede hacer referencia a variables definidas en la clase derivada, pero estas aún no se inicializan con los valores especificados por sus inicializadores o establecidos en el constructor de la clase derivada. El último problema también se aplica a C#, pero de una forma menos crítica, ya que en C# los métodos no se pueden anular de forma predeterminada.
Ambos lenguajes utilizan principalmente la recolección de basura como un medio para recuperar recursos de memoria, en lugar de una desasignación explícita de memoria. En ambos casos, si un objeto contiene recursos de diferentes tipos además de la memoria, como identificadores de archivos, recursos gráficos, etc., entonces se le debe notificar explícitamente cuando la aplicación ya no lo usa. Tanto C# como Java ofrecen interfaces para dicha eliminación determinista y tanto C# como Java (desde Java 7) cuentan con instrucciones de administración automática de recursos que invocarán automáticamente los métodos de eliminación/cierre en esas interfaces.
Métodos y propiedades | Java | DO# |
---|---|---|
Importaciones estáticas | Sí | Sí [57] |
Métodos virtuales | Virtual por defecto | No virtual por defecto |
Abstracto | Sí | Sí |
Caza de focas | Sí | Sí |
Implementación de interfaz explícita | Métodos predeterminados | Sí [58] |
Parámetros de valor (entrada) | Sí | Sí |
Parámetros de referencia (entrada/salida) | No | Sí |
Parámetros de salida (output) | No | Sí |
Parámetros constantes (inmutables) | Sí; parámetros finales | Sí [59] |
Métodos variádicos | Sí | Sí |
Argumentos opcionales | No; [60] En lugar de sobrecarga de métodos o varargs | Sí |
Argumentos nombrados | Sí; con anotaciones | Sí |
Métodos generadores | No; pero vea Stream API | Sí |
Métodos de extensión/predeterminados | Sí | Sí |
Métodos condicionales | No | Sí |
Métodos parciales | No | Sí |
Al utilizar un designador this especial en el primer parámetro de un método, C# permite que el método actúe como si fuera un método miembro del tipo del primer parámetro. Esta extensión de la clase externa es puramente sintáctica. El método de extensión debe declararse estático y definirse dentro de una clase puramente estática. El método debe obedecer cualquier restricción de acceso a miembros como cualquier otro método externo a la clase; por lo tanto, los métodos estáticos no pueden romper la encapsulación de objetos. [61] [62] La "extensión" solo está activa dentro de los ámbitos donde se ha importado el espacio de nombres de la clase host estática.
Desde Java 8, Java tiene una característica similar llamada métodos predeterminados , que son métodos con un cuerpo declarado en interfaces. A diferencia de los métodos de extensión de C#, los métodos predeterminados de Java son métodos de instancia en la interfaz que los declara. La definición de métodos predeterminados en clases que implementan la interfaz es opcional: si la clase no define el método, se utiliza la definición predeterminada en su lugar.
Tanto los métodos de extensión de C# como los métodos predeterminados de Java permiten que una clase anule la implementación predeterminada del método de extensión/predeterminado, respectivamente. En ambos lenguajes, esta anulación se logra definiendo un método en la clase que debería utilizar una implementación alternativa del método.
Las reglas de alcance de C# definen que si se encuentra un método coincidente en una clase, este tiene prioridad sobre un método de extensión coincidente. En Java, se supone que cualquier clase declarada para implementar una interfaz con un método predeterminado tiene las implementaciones de los métodos predeterminados, a menos que la clase implemente el método en sí.
En relación con las clases parciales, C# permite especificar métodos parciales dentro de clases parciales. Un método parcial es una declaración intencional de un método con varias restricciones en la firma. Las restricciones garantizan que si ninguna parte de la clase proporciona una definición, entonces el método y cada llamada a este se pueden borrar de forma segura. [63] Esta característica permite que el código proporcione una gran cantidad de puntos de intercepción (como el patrón de diseño del método de plantilla GoF ) sin pagar ninguna sobrecarga de tiempo de ejecución si otra parte de la clase no está utilizando estos puntos de extensión en tiempo de compilación. Java no tiene un concepto correspondiente.
Los métodos en C# no son virtuales de manera predeterminada y, si se desea, deben declararse como virtuales explícitamente. En Java, todos los métodos no privados y no estáticos son virtuales. La virtualidad garantiza que siempre se llamará a la anulación más reciente del método, pero implica un cierto costo de tiempo de ejecución en la invocación, ya que estas invocaciones normalmente no se pueden incluir en línea y requieren una llamada indirecta a través de la tabla de métodos virtuales . Sin embargo, algunas implementaciones de JVM, incluida la implementación de referencia de Oracle, implementan la inclusión en línea de los métodos virtuales llamados con más frecuencia.
Los métodos de Java son virtuales de manera predeterminada (aunque se pueden sellar utilizando el modificador final para impedir su anulación). No hay forma de permitir que las clases derivadas definan un método nuevo y no relacionado con el mismo nombre.
Esto significa que, de manera predeterminada en Java, y solo cuando está habilitado explícitamente en C#, se pueden definir nuevos métodos en una clase derivada con el mismo nombre y firma que los de su clase base. Cuando se llama al método en una referencia de superclase de dicho objeto, se llamará a la implementación anulada "más profunda" del método de la clase base de acuerdo con la subclase específica del objeto al que se hace referencia.
En algunos casos, cuando una subclase introduce un método con el mismo nombre y firma que un método ya presente en la clase base , pueden surgir problemas. En Java, esto significa que el método de la clase derivada anulará implícitamente el método de la clase base, aunque esa no sea la intención de los diseñadores de ninguna de las clases.
Para mitigar esto, C# requiere que si un método tiene la intención de anular un método heredado, se debe especificar la palabra clave override . De lo contrario, el método "ocultará" el método heredado. Si la palabra clave no está presente, se emite una advertencia del compilador a tal efecto, que se puede silenciar especificando la palabra clave new . Esto evita el problema que puede surgir de una clase base que se extiende con un método no privado (es decir, una parte heredada del espacio de nombres) cuya firma ya está en uso por una clase derivada. Java tiene una comprobación de compilador similar en forma de anotación de método, pero no es obligatoria y, en su ausencia, la mayoría de los compiladores no proporcionarán comentarios (pero el método se anulará).@Override
En Java, es posible evitar la reasignación de una variable local o un parámetro de método mediante el uso de la final
palabra clave. La aplicación de esta palabra clave a una variable de tipo primitivo hace que la variable se vuelva inmutable. Sin embargo, la aplicación final
a una variable de tipo de referencia solo evita que se le asigne otro objeto. No evitará que se muten los datos contenidos en el objeto. A partir de C#7, es posible evitar la reasignación de un parámetro de método mediante el uso de la palabra clave in , sin embargo, esta palabra clave no se puede utilizar en variables locales. Al igual que con Java, la aplicación de in a un parámetro solo evita que el parámetro se reasigne a un valor diferente. Todavía es posible mutar los datos contenidos en el objeto. [64]
Java | DO# |
---|---|
public int addOne ( final int x ) { x ++ ; // ERROR: no se puede reasignar una variable final return x ; } public List addOne ( final List < Integer > list ) { list.add ( 1 ); // OK: todavía es posible modificar una variable final ( tipo de referencia) return list ; } | public int AddOne ( in int x ) { x ++ ; // ERROR: no se puede reasignar un parámetro de solo lectura return x ; } public List < int > AddOne ( in List < int > list ) { list . Add ( 1 ); // OK: todavía es posible modificar un // parámetro de solo lectura (tipo de referencia) return list ; } |
Ambos lenguajes no admiten la característica esencial de corrección constante que existe en C / C++ , que hace que un método sea constante.
Java define la palabra "constante" arbitrariamente como un campo. Como convención, estos nombres de variable se escriben solo en mayúsculas y las palabras se separan con un guión bajo, pero el lenguaje Java no insiste en esto. Un parámetro que solo tiene no se considera una constante, aunque puede serlo en el caso de un tipo de datos primitivo o una clase inmutable , como una .static final
final
String
Cualquier método de C# declarado como que devuelve IEnumerable , IEnumerator o las versiones genéricas de estas interfaces se puede implementar utilizando la sintaxis yield . Esta es una forma de continuaciones limitadas generadas por el compilador y puede reducir drásticamente el código necesario para recorrer o generar secuencias, aunque ese código simplemente lo genera el compilador. La característica también se puede utilizar para implementar secuencias infinitas, por ejemplo, la secuencia de números de Fibonacci .
Java no tiene una característica equivalente. En cambio, los generadores se definen normalmente proporcionando una implementación especializada de una colección conocida o una interfaz iterable, que calculará cada elemento a pedido. Para que un generador de este tipo se utilice en una declaración for each , debe implementar interface java.lang.Iterable
.
Véase también el ejemplo de secuencia de Fibonacci a continuación.
C# también tiene una implementación de interfaz explícita que permite a una clase implementar específicamente métodos de una interfaz , separarlos de sus propios métodos de clase o proporcionar diferentes implementaciones para dos métodos con el mismo nombre y firma heredados de dos interfaces base.
En cualquiera de los lenguajes, si se especifica un método (o propiedad en C#) con el mismo nombre y firma en varias interfaces, los miembros entrarán en conflicto cuando se diseñe una clase que implemente esas interfaces. Una implementación implementará por defecto un método común para todas las interfaces. Si se necesitan implementaciones separadas (porque los métodos sirven para propósitos diferentes o porque los valores de retorno difieren entre las interfaces), la implementación explícita de la interfaz de C# resolverá el problema, aunque permitirá diferentes resultados para el mismo método, dependiendo de la conversión actual del objeto. En Java no hay otra forma de resolver este problema que refactorizando una o más de las interfaces para evitar conflictos de nombres. [58]
En Java, los argumentos de los tipos primitivos (por ejemplo, int, double) a un método se pasan por valor, mientras que los objetos se pasan por referencia. Esto significa que un método opera sobre copias de los tipos primitivos que se le pasan en lugar de sobre las variables reales. Por el contrario, en algunos casos los objetos reales pueden cambiar. En el siguiente ejemplo, el objeto String no se cambia. Se cambia el objeto de la clase 'a'.
En C#, es posible hacer cumplir una referencia con la palabra clave ref , de forma similar a C++ y en cierto sentido a C. Esta característica de C# es particularmente útil cuando se desea crear un método que devuelva más de un objeto. En Java, no se puede intentar devolver varios valores de un método a menos que se utilice un contenedor, en este caso llamado "Ref". [65]
Java | DO# |
---|---|
clase PassByRefTest { clase estática Ref < T > { T val ; Ref ( T val ) { this . val = val ; } } static void changeMe ( Ref < String > s ) { s . val = "Cambiado" ; } intercambio de vacío estático ( Ref < Entero > x , Ref < Entero > y ) { int temp = x . val ; x . valor = y . valor ; y . valor = temperatura ; } public static void main ( String [] args ) { var a = new Ref ( 5 ); var b = new Ref ( 10 ); var s = new Ref ( "aún sin cambios" ); swap ( a , b ); changeMe ( s ); Sistema . out . println ( "a = " + a . val + ", " + "b = " + b . val + ", " + "s = " + s . val ); } } | clase PassByRefTest { public static void ChangeMe ( out string s ) { s = "Cambiado" ; } público estático void Swap ( ref int x , ref int y ) { int temp = x ; x = y ; y = temperatura ; } public static void Main ( string [] args ) { int a = 5 ; int b = 10 ; string s = "aún sin cambios" ; Intercambiar ( ref a , ref b ); ChangeMe ( fuera s ); Sistema . Consola . WriteLine ( "a = " + a + ", " + "b = " + b + ", " + "s = " + s ); } } |
a = 10, b = 5, s = Changed | a = 10, b = 5, s = Changed |
Excepciones | Java | DO# |
---|---|---|
Excepciones comprobadas | Sí | No |
Intentar-atrapar-finalmente | Sí | Sí |
Filtros de excepción | No | Sí [66] |
Java admite excepciones controladas (junto con excepciones no controladas). C# solo admite excepciones no controladas. Las excepciones controladas obligan al programador a declarar la excepción lanzada en un método o a capturar la excepción lanzada mediante una cláusula try-catch .
Las excepciones comprobadas pueden fomentar una buena práctica de programación, garantizando que se solucionen todos los errores. Sin embargo, Anders Hejlsberg , arquitecto jefe del lenguaje C#, sostiene que, hasta cierto punto, fueron un experimento en Java y que no se ha demostrado que valgan la pena, excepto en pequeños programas de ejemplo. [67] [68]
Una crítica es que las excepciones comprobadas alientan a los programadores a usar un bloque catch vacío ( ), [69] que silenciosamente se traga las excepciones, en lugar de dejar que las excepciones se propaguen a una rutina de manejo de excepciones de nivel superior. En algunos casos, sin embargo, se puede aplicar en su lugar el encadenamiento de excepciones , volviendo a lanzar la excepción en una excepción contenedora. Por ejemplo, si se cambia un objeto para acceder a una base de datos en lugar de a un archivo, se podría capturar un y volver a lanzarlo como un , ya que el llamador puede no necesitar conocer el funcionamiento interno del objeto.catch (Exception e) {}
SQLException
IOException
Sin embargo, no todos los programadores están de acuerdo con esta postura. James Gosling y otros sostienen que las excepciones controladas son útiles y que su mal uso ha causado los problemas. Es posible capturar excepciones de forma silenciosa, sí, pero se debe indicar explícitamente qué hacer con ellas, a diferencia de las excepciones no controladas que permiten no hacer nada de forma predeterminada. Se pueden ignorar, pero se debe escribir código explícitamente para ignorarlas. [70] [71]
También existen diferencias entre los dos lenguajes en el tratamiento de la try-finally
sentencia. El bloque finally siempre se ejecuta, incluso si el bloque try contiene sentencias de paso de control como throw o return . En Java, esto puede dar lugar a un comportamiento inesperado si el bloque try es abandonado por una sentencia return con algún valor y, a continuación, el bloque finally que se ejecuta a continuación también es abandonado por una sentencia return con un valor diferente. C# resuelve este problema al prohibir cualquier sentencia de paso de control como return o break en el bloque finally .
Una razón común para usar try-finally
bloques es proteger el código de administración de recursos, garantizando así la liberación de recursos valiosos en el bloque finally. C# presenta la declaración using como una abreviatura sintáctica para este escenario común, en el que siempre se llama al método del objeto de la instrucción using .Dispose()
Una diferencia bastante sutil es el momento en que se crea un seguimiento de la pila cuando se lanza una excepción. En Java, el seguimiento de la pila se crea en el momento en que se crea la excepción.
clase Foo { Excepción arriba = nueva Excepción (); int foo () lanza Excepción { lanzar arriba ; } }
La excepción en la declaración anterior siempre contendrá el seguimiento de la pila del constructor, sin importar con qué frecuencia se llame a foo. En C#, por otro lado, el seguimiento de la pila se crea en el momento en que se ejecuta "throw".
clase Foo { Excepción e = nueva Excepción (); int foo () { try { throw e ; } catch ( Excepción e ) { throw ; } } }
En el código anterior, la excepción contendrá el seguimiento de la pila de la primera línea lanzada. Al capturar una excepción, hay dos opciones en caso de que la excepción deba volver a lanzarse: lanzar simplemente volverá a lanzar la excepción original con la pila original, mientras que habría creado un nuevo seguimiento de la pila.throw e
Java permite que el flujo de control abandone el bloque finally de una sentencia try , independientemente de la forma en que se haya introducido. Esto puede provocar que otra sentencia de flujo de control (como return ) finalice a mitad de la ejecución. Por ejemplo:
int foo () { try { return 0 ; } finalmente { return 1 ; } }
En el código anterior, la declaración de retorno dentro del bloque try hace que el control lo abandone y, por lo tanto, el bloque finally se ejecuta antes de que ocurra el retorno real. Sin embargo, el bloque finally también realiza un retorno. Por lo tanto, el retorno original que provocó que se ingresara no se ejecuta y el método anterior devuelve 1 en lugar de 0. Hablando informalmente, intenta devolver 0 pero finalmente devuelve 1.
C# no permite ninguna instrucción que permita que el flujo de control abandone el bloque finally de forma prematura, excepto throw . En particular, no se permite return en absoluto, goto no se permite si la etiqueta de destino está fuera del bloque finally , y continue y break no se permiten si el bucle envolvente más cercano está fuera del bloque finally .
En el campo de los genéricos, los dos lenguajes muestran una similitud sintáctica superficial, pero tienen profundas diferencias subyacentes.
Genéricos | Java | DO# |
---|---|---|
Implementación | Borrado de tipo | Cosificación |
Realización en tiempo de ejecución | No | Sí |
Variación de tipo | Sitio de uso | Sitio de declaración (solo en interfaces) |
Restricción de tipo de referencia | Sí; implícito | Sí |
Restricción de tipo primitivo/valor | No | Sí |
Restricción del constructor | No | Sí (solo para constructor sin parámetros) |
Restricción de subtipo | Sí | Sí |
Restricción de supertipo | Sí | No |
Compatibilidad con la migración | Sí | No |
Los genéricos en Java son una construcción exclusiva del lenguaje; se implementan únicamente en el compilador. Los archivos de clase generados incluyen firmas genéricas únicamente en forma de metadatos (lo que permite al compilador compilar nuevas clases en función de ellos). El entorno de ejecución no tiene conocimiento del sistema de tipos genéricos; los genéricos no forman parte de la máquina virtual Java (JVM). En cambio, las clases y los métodos genéricos se transforman durante la compilación a través de un proceso denominado borrado de tipos . Durante este proceso, el compilador reemplaza todos los tipos genéricos con su versión original e inserta conversiones/verificaciones de forma adecuada en el código del cliente donde se utilizan el tipo y sus métodos. El código de bytes resultante no contendrá referencias a ningún tipo o parámetro genérico (consulte también Genéricos en Java ).
La especificación del lenguaje Java prohíbe intencionalmente ciertos usos de genéricos; esto es necesario para permitir la implementación de genéricos a través del borrado de tipos y para permitir la compatibilidad de la migración. [72] La investigación para agregar genéricos reificados a la plataforma Java está en curso, como parte del Proyecto Valhalla .
C# se basa en el soporte para genéricos del sistema de ejecución virtual, es decir, no es solo una característica del lenguaje. El lenguaje es simplemente una interfaz para el soporte de genéricos entre lenguajes en el CLR . Durante la compilación, se verifica que los genéricos sean correctos, pero la generación de código para implementar los genéricos se pospone al momento de la carga de la clase. El código del cliente (código que invoca métodos/propiedades genéricos) se compila por completo y puede asumir con seguridad que los genéricos son seguros para los tipos. Esto se llama reificación . En tiempo de ejecución, cuando se encuentra por primera vez un conjunto único de parámetros de tipo para una clase/método/delegado genérico, el cargador/verificador de clases sintetizará un descriptor de clase concreto y generará implementaciones de métodos. Durante la generación de implementaciones de métodos, todos los tipos de referencia se considerarán un solo tipo, ya que los tipos de referencia pueden compartir de manera segura las mismas implementaciones. Esto es simplemente con el propósito de implementar código. Diferentes conjuntos de tipos de referencia seguirán teniendo descriptores de tipo únicos; sus tablas de métodos simplemente apuntarán al mismo código.
La siguiente lista ilustra algunas diferencias entre Java y C# en la gestión de genéricos. No es exhaustiva: [73]
Java | DO# |
---|---|
Las comprobaciones de tipo y las conversiones descendentes se inyectan en el código del cliente (el código que hace referencia a los genéricos). En comparación con el código no genérico con conversiones manuales, estas conversiones serán las mismas, [74] pero, en comparación con el código verificado en tiempo de compilación que no necesitaría conversiones y comprobaciones en tiempo de ejecución, estas operaciones representan una sobrecarga de rendimiento. | Los genéricos de C#/.NET garantizan la seguridad de tipos y se verifican en tiempo de compilación, lo que hace que no sean necesarias comprobaciones o conversiones adicionales en tiempo de ejecución. Por lo tanto, el código genérico se ejecutará más rápido que el código no genérico (o de tipo borrado) que requiere conversiones al manejar objetos no genéricos o de tipo borrado. |
No se pueden utilizar tipos primitivos como parámetros de tipo; en su lugar, el desarrollador debe utilizar el tipo contenedor correspondiente al tipo primitivo. Esto genera una sobrecarga de rendimiento adicional al requerir conversiones de boxing y unboxing, así como una presión de memoria y recolección de basura, ya que los contenedores se asignarán en el montón en lugar de en la pila. | Los tipos primitivos y de valor se permiten como parámetros de tipo en las realizaciones genéricas. En tiempo de ejecución, el código se sintetizará y compilará para cada combinación única de parámetros de tipo en el primer uso. Los genéricos que se realizan con tipos primitivos o de valor no requieren conversiones de boxing o unboxing. |
No se permiten excepciones genéricas [75] y no se puede utilizar un parámetro de tipo en una cláusula catch [76] | Se pueden definir excepciones genéricas y utilizarlas en cláusulas catch |
Los miembros estáticos se comparten entre todas las realizaciones genéricas [77] (durante el borrado de tipo, todas las realizaciones se pliegan en una sola clase) | Los miembros estáticos son independientes para cada realización genérica. Una realización genérica es una clase única. |
Los parámetros de tipo no se pueden utilizar en declaraciones de campos/métodos estáticos ni en definiciones de clases internas estáticas. | No hay restricciones en el uso de parámetros de tipo |
No se puede crear una matriz donde el tipo de componente sea una realización genérica (tipo parametrizado concreto)Par < Cadena , Cadena >[] diezPares = nuevo Par [ 10 ] ; //OK | Una realización genérica es un ciudadano de primera clase y se puede utilizar como cualquier otra clase; también un componente de matrizobjeto diezPares = nuevo Par < int , string > [ 10 ]; // OK |
No se puede crear una matriz donde el tipo de componente sea un parámetro de tipo, pero es válido crear una Object matriz y realizar una conversión de tipos en la nueva matriz para lograr el mismo efecto.clase pública Lookup < K , V > { pública V [] getEmptyValues ( clave K ) { return ( V [] ) nuevo Object [ 0 ] ; // OK } } Cuando un parámetro de tipo genérico está bajo restricciones de herencia, se puede utilizar el tipo de restricción en lugar de clase pública Lookup < K , V extiende Comparable < V >> { pública V [] getEmptyValues ( clave K ) { return ( V [] ) nuevo Comparable [ 0 ] ; } } | Los parámetros de tipo representan clases reales y discretas y pueden usarse como cualquier otro tipo dentro de la definición genérica.clase pública Lookup < K , V > { pública V [] GetEmptyValues ( clave K ) { devolver nueva V [ 0 ]; // OK } } |
No existe ningún literal de clase para una realización concreta de un tipo genérico | Una realización genérica es una clase real. |
No se permite instanceof con parámetros de tipo o realizaciones genéricas concretas | Los operadores is y as funcionan de la misma manera para los parámetros de tipo que para cualquier otro tipo. |
No se pueden crear nuevas instancias utilizando un parámetro de tipo como tipo | Con una restricción de constructor, los métodos genéricos o los métodos de clases genéricas pueden crear instancias de clases que tienen constructores predeterminados. |
La información de tipo se borra durante la compilación. Se deben utilizar extensiones especiales de reflexión para descubrir el tipo original. | La información de tipo sobre los tipos genéricos de C# se conserva completamente en tiempo de ejecución y permite la compatibilidad total con la reflexión y la creación de instancias de tipos genéricos. |
La reflexión no se puede utilizar para construir nuevas realizaciones genéricas. Durante la compilación, se inyecta código adicional (conversiones de tipos) en el código cliente de los genéricos. Esto impide la creación de nuevas realizaciones más adelante. | La reflexión se puede utilizar para crear nuevas realizaciones para nuevas combinaciones de parámetros de tipo. |
C# permite el uso de genéricos directamente para tipos primitivos. Java, en cambio, permite el uso de tipos encajonados como parámetros de tipo (por ejemplo, en lugar de ). Esto tiene un costo, ya que todos esos valores deben encajonarse o desencajonarse cuando se usan, y todos deben asignarse en el montón. Sin embargo, un tipo genérico se puede especializar con un tipo de matriz de un tipo primitivo en Java, por ejemplo, está permitido. [78]
Varias bibliotecas de terceros implementaron las colecciones básicas en Java con matrices primitivas de respaldo para preservar el tiempo de ejecución y la optimización de la memoria que brindan los tipos primitivos. [79]List<Integer>
List<int>
List<int[]>
El diseño de borrado de tipos de Java estuvo motivado por un requisito de diseño para lograr compatibilidad con la migración (que no debe confundirse con compatibilidad con versiones anteriores ). En particular, el requisito original era " ... debería haber una ruta de migración clara y demostrable para las API de colecciones que se introdujeron en la plataforma Java 2 ". [46] Esto se diseñó para que cualquier nueva colección genérica pudiera pasarse a métodos que esperaban una de las clases de colección preexistentes. [80]
Los genéricos de C# se introdujeron en el lenguaje conservando la compatibilidad total con versiones anteriores, pero no se conservó la compatibilidad total con la migración : el código antiguo (anterior a C# 2.0) se ejecuta sin cambios en el nuevo entorno de ejecución que reconoce los genéricos sin recompilación. En cuanto a la compatibilidad con la migración , se desarrollaron nuevas clases e interfaces de colección genéricas que complementaron las colecciones no genéricas de .NET 1.x en lugar de reemplazarlas. Además de las interfaces de colección genéricas, las nuevas clases de colección genéricas implementan las interfaces de colección no genéricas siempre que sea posible. Esto evita el uso de nuevas colecciones genéricas con métodos preexistentes (que no reconocen los genéricos), si esos métodos están codificados para usar las clases de colección .
Ambos lenguajes admiten la covarianza y la contravarianza. Java tiene una varianza en el sitio de uso que permite que una única clase genérica declare miembros utilizando tanto la covarianza como la contravarianza. C# tiene una varianza en el sitio de definición para las interfaces genéricas y los delegados. La varianza no se admite directamente en las clases, pero se admite a través de su implementación de interfaces variantes. C# también tiene compatibilidad con la covarianza en el sitio de uso para métodos y delegados.
Programación funcional | Java | DO# |
---|---|---|
Referencias de métodos | Sí [10] | Sí |
Cierres | No todas las lambdas introducen un nuevo nivel de alcance. Todas las variables a las que se hace referencia deben ser efectivamente finales. | Sí |
Expresiones lambda | Sí [81] | Sí |
Árboles de expresión | No | Sí |
Lenguaje de consulta genérico/API | Sí; API de Java Stream (Monad) [82] | Sí |
Optimizaciones del compilador de recursión de cola | No [83] | Sólo en x64 [84] |
Un cierre es una función en línea que captura variables de su ámbito léxico.
C# admite cierres como métodos anónimos o expresiones lambda con semántica de cierre con todas las funciones . [85] [86]
En Java, las clases internas anónimas seguirán siendo la forma preferida de emular cierres hasta que Java 8 se convierta en el nuevo estándar. Se trata de una construcción más verbosa. Este enfoque también tiene algunas diferencias en comparación con los cierres reales, en particular un acceso más controlado a las variables desde los ámbitos que las encierran: solo se puede hacer referencia a los miembros finales. Sin embargo, Java 8 introduce lambdas que heredan por completo el ámbito actual y, de hecho, no introducen un nuevo ámbito.
Cuando se puede pasar una referencia a un método para su posterior ejecución, surge un problema sobre qué hacer cuando el método tiene referencias a variables/parámetros en su ámbito léxico. Los cierres de C# pueden acceder a cualquier variable/parámetro de su ámbito léxico. En las clases internas anónimas de Java, solo se permiten referencias a miembros finales del ámbito léxico, por lo que se requiere que el desarrollador marque qué variables poner a disposición y en qué estado (posiblemente se requiera boxing).
C# y Java cuentan con un tipo especial de cierres en línea llamados lambdas . Se trata de métodos anónimos: tienen una firma y un cuerpo, pero no tienen nombre. Se utilizan principalmente para especificar argumentos con valores de función locales en llamadas a otros métodos, una técnica asociada principalmente con la programación funcional .
A diferencia de Java, C# permite el uso de funciones lambda como una forma de definir estructuras de datos especiales llamadas árboles de expresión. El hecho de que se consideren como una función ejecutable o como una estructura de datos depende de la inferencia de tipo del compilador y del tipo de variable o parámetro al que se les asigna o convierte. Las funciones lambda y los árboles de expresión desempeñan papeles clave en Language Integrated Query (LINQ).
Metadatos | Java | DO# |
---|---|---|
Anotaciones/atributos de metadatos | Basado en interfaz; se pueden crear anotaciones definidas por el usuario [87] | Basado en clases |
Argumentos posicionales | No; a menos que haya un solo argumento | Sí |
Argumentos nombrados | Sí | Sí |
Valores predeterminados | En la definición | A través de la inicialización |
Tipos anidados | Sí | Sí |
Especialización | No | Sí |
Metadatos condicionales | No | Sí |
Preprocesamiento , compilación y empaquetado | Java | DO# |
---|---|---|
Espacios de nombres | Paquetes | Espacios de nombres |
Contenido del archivo | Restringido | Gratis |
Embalaje | Paquete | Visibilidad pública/interna de los miembros del espacio de nombres, que el sistema de compilación traduce en módulos y ensamblajes en el nivel CLR |
Ruta de búsqueda de clases/ensamblado | Ruta de clase | Tanto en tiempo de compilación como en tiempo de ejecución [88] [89] |
Compilación condicional | No; pero véase Apache Ant [90] | Sí |
Errores/advertencias personalizadas | Sí; Procesador de anotaciones | Sí |
Regiones explícitas | No | Sí |
En C#, los espacios de nombres son similares a los de C++ . A diferencia de los nombres de paquetes en Java, un espacio de nombres no está vinculado de ninguna manera a la ubicación del archivo fuente. Si bien no es estrictamente necesario que la ubicación de un archivo fuente de Java refleje la estructura del directorio del paquete, es la organización convencional.
Ambos lenguajes permiten la importación de clases (por ejemplo, en Java), lo que permite hacer referencia a una clase utilizando únicamente su nombre. A veces, existen clases con el mismo nombre en varios espacios de nombres o paquetes. Se puede hacer referencia a dichas clases utilizando nombres totalmente calificados o importando únicamente clases seleccionadas con nombres diferentes. Para ello, Java permite importar una única clase (por ejemplo, ). C# permite importar clases con un nuevo nombre local utilizando la siguiente sintaxis: . También permite importar especializaciones de clases en forma de .import java.util.*
import java.util.List
using Console = System.Console
using IntList = System.Collections.Generic.List<int>
Ambos lenguajes tienen una sintaxis de importación estática que permite usar el nombre corto de algunos o todos los métodos/campos estáticos en una clase (por ejemplo, permitiendo que foo(bar)
where foo()
pueda importarse estáticamente desde otra clase). C# tiene una sintaxis de clase estática (que no debe confundirse con las clases internas estáticas en Java), que restringe una clase para que solo contenga métodos estáticos. C# 3.0 introduce métodos de extensión para permitir que los usuarios agreguen estáticamente un método a un tipo (por ejemplo, permitiendo que foo.bar()
where bar()
pueda ser un método de extensión importado que funcione en el tipo de foo ).
El compilador Java de Sun Microsystems requiere que el nombre de un archivo fuente coincida con la única clase pública que contiene, mientras que C# permite múltiples clases públicas en el mismo archivo y no impone restricciones en cuanto al nombre del archivo. C# 2.0 y versiones posteriores permiten dividir una definición de clase en varios archivos mediante el uso de la palabra clave partial en el código fuente. En Java, una clase pública siempre estará en su propio archivo fuente. En C#, los archivos de código fuente y la separación de unidades lógicas no están estrechamente relacionados.
A diferencia de Java, C# implementa la compilación condicional mediante directivas de preprocesador . También proporciona un atributo Conditional para definir métodos que solo se llaman cuando se define una constante de compilación determinada. De esta manera, las aserciones se pueden proporcionar como una característica del marco con el método , que solo se evalúa cuando se define la constante DEBUG . Desde la versión 1.4, Java proporciona una característica del lenguaje para las aserciones, que se desactivan en tiempo de ejecución de forma predeterminada, pero se pueden habilitar mediante el modificador o al invocar la JVM. Debug.Assert()
-enableassertions
-ea
Ambos lenguajes incluyen mecanismos de sincronización de subprocesos como parte de su sintaxis.
Subprocesamiento y sincronización | Java | DO# |
---|---|---|
Trapos | Sí | Sí |
Grupo de subprocesos | Sí | Sí |
Paralelismo basado en tareas | Sí [91] | Sí [92] |
Semáforos | Sí | Sí |
Monitores | Sí | Sí |
Variables locales del hilo | Sí | Sí; ThreadStaticAttribute y claseThreadLocal<T> |
Con .NET Framework 4.0, se introdujo un nuevo modelo de programación basado en tareas para reemplazar el modelo asincrónico basado en eventos existente. La API se basa en las tareas y las clases. Las tareas se pueden componer y encadenar.Task<T>
Por convención, cada método que devuelve una Tarea debe tener su nombre posfijo con Async .
clase pública estática SomeAsyncCode { Tarea pública estática < XDocument > GetContentAsync () { HttpClient httpClient = new HttpClient (); return httpClient . GetStringAsync ( "www.contoso.com" ). ContinueWith (( tarea ) => { cadena responseBodyAsText = tarea . Result ; return XDocument . Parse ( responseBodyAsText ); }); } } var t = SomeAsyncCode . GetContentAsync (). ContinueWith (( tarea ) => { var xmlDocument = tarea . Resultado ; }); t . Inicio ();
En C# 5 se introdujo un conjunto de extensiones de lenguaje y compilador para facilitar el trabajo con el modelo de tareas. Estas extensiones de lenguaje incluían la noción de métodos asincrónicos y la declaración await que hacen que el flujo del programa parezca sincrónico.
clase pública estática SomeAsyncCode { pública estática asincrónica Tarea < XDocument > GetContentAsync () { HttpClient httpClient = new HttpClient (); cadena responseBodyAsText = await httpClient . GetStringAsync ( "www.contoso.com" ); devolver XDocument . Parse ( responseBodyAsText ); } } var xmlDocument = await SomeAsyncCode .GetContentAsync () ; //La tarea se iniciará en la llamada con espera.
A partir de este azúcar sintáctico, el compilador de C# genera una máquina de estados que maneja las continuaciones necesarias sin que los desarrolladores tengan que pensar en ello.
Java admite subprocesos desde JDK 1.0. Java ofrece una gran versatilidad para ejecutar subprocesos, a menudo denominados tareas. Esto se logra implementando una interfaz funcional (una java.lang.Runnable
interfaz) que define un único método void no-args como se muestra en el siguiente ejemplo:
var myThread = nuevo hilo (() -> { var nombreHilo = Hilo.currentThread (). getName ( ) ; Sistema . out . println ( "Hola " + threadName ); });miHilo.start ( ) ;
De manera similar a C#, Java tiene un mecanismo de nivel superior para trabajar con subprocesos. Los ejecutores pueden ejecutar tareas asincrónicas y también administrar un grupo de subprocesos. Todos los subprocesos de una instancia de ExecutorServices se manejan en un pool . Esta instancia de ExecutorService se reutilizará en segundo plano para tareas recurrentes, por lo que es posible ejecutar tantas tareas concurrentes como desee el programador durante el ciclo de vida de la aplicación utilizando una única instancia de servicio de ejecutor.
Así es como se ve el primer ejemplo de hilo usando ejecutores:
EjecutorService ejecutor = Ejecutores . newSingleThreadExecutor (); ejecutor . enviar (() -> { var nombreHilo = Hilo.currentThread (). getName ( ) ; Sistema . out . println ( "Hola " + threadName ); });
La instancia ExecutorService también admite una interfaz Callable , otra interfaz de método único como Runnable , pero la firma del método contenido de Callable devuelve un valor. De esta manera, la expresión lambda también debe devolver un valor, como el siguiente ejemplo de llamada a un sitio web de forma asincrónica como el ejemplo de C#.
EjecutorService ejecutor = Ejecutores . newSingleThreadExecutor (); Futuro < String > contentAsync = ejecutor .submit ( () -> { HttpRequest httpReq = HttpRequest . newBuilder () .uri ( nueva URI ( "www.graalvm.org" ) ) . construir (); devuelve HttpClient .newHttpClient ( ) . enviar ( httpReq , BodyHandlers . ofString ()) . cuerpo ();}); var webPageResult = contentAsync . conseguir ();
Al llamar al método get()
se bloquea el hilo actual y se espera hasta que se complete la llamada antes de devolver el valor (en el ejemplo, el contenido de una página web):
En el siguiente ejemplo, se utilizan un método y una clase. Esta envoltura es solo para que sea similar al ejemplo de C#, ya que Java no tiene palabras clave como async para la firma del método.
clase pública estática SomeAsyncCode { static ExecutorService ejecutor = Ejecutores.newSingleThreadExecutor ( ) ; público estático Futuro < String > getContentAsync (){ devuelve ejecutor .submit (() - > { HttpRequest httpReq = HttpRequest . newBuilder () .uri ( nueva URI ( "www.graalvm.org" ) ) . construir (); devuelve HttpClient .newHttpClient ( ) . enviar ( httpReq , BodyHandlers . ofString ()) . cuerpo (); }); }}var webPageResult = SomeAsyncCode . getContentAsync (). conseguir ();
Para soportar adecuadamente las aplicaciones en el campo del cálculo matemático y financiero, existen varias características del lenguaje. [93]
La palabra clave strictfp de Java permite realizar cálculos estrictos de punto flotante para una región de código. Los cálculos estrictos de punto flotante requieren que, incluso si una plataforma ofrece una precisión más alta durante los cálculos, los resultados intermedios se deben convertir a simple/doble. Esto garantiza que los cálculos estrictos de punto flotante devuelvan exactamente el mismo resultado en todas las plataformas. Sin un punto flotante estricto, una implementación de plataforma es libre de usar una precisión más alta para los resultados intermedios durante el cálculo. C# permite que una implementación para una arquitectura de hardware dada use siempre una precisión más alta para los resultados intermedios si está disponible, es decir, C# no permite que el programador fuerce opcionalmente que los resultados intermedios usen la precisión potencialmente más baja de simple/doble. [94]
Aunque la aritmética de punto flotante de Java se basa en gran medida en el estándar IEEE 754 (estándar para aritmética de punto flotante binario), ciertas características no son compatibles incluso cuando se utiliza el modificador strictfp, como los indicadores de excepción y los redondeos dirigidos, capacidades exigidas por el estándar IEEE 754 (consulte Crítica de Java, aritmética de punto flotante ).
C# proporciona un tipo decimal integrado, [95] que tiene mayor precisión (pero menor rango) que el double de Java/C#. El tipo decimal es un tipo de datos de 128 bits adecuado para cálculos financieros y monetarios. El tipo decimal puede representar valores que van desde 1,0 × 10 −28 hasta aproximadamente 7,9 × 10 28 con 28–29 dígitos significativos. [96] La estructura utiliza la sobrecarga de operadores de C# para que los decimales se puedan manipular utilizando operadores como + , - , * y / , como otros tipos de datos primitivos.
Los tipos BigDecimal
y BigInteger
que se proporcionan con Java permiten la representación con precisión arbitraria de números decimales y números enteros, respectivamente. La biblioteca estándar de Java no tiene clases para trabajar con números complejos.
Los tipos BigInteger
, [3] y Complex
[97] que se proporcionan con C# permiten la representación y manipulación de números enteros y complejos de precisión arbitraria, respectivamente. Las estructuras utilizan la sobrecarga de operadores de C# para que las instancias se puedan manipular utilizando operadores como + , - , * y / , como otros tipos de datos primitivos. La biblioteca estándar de C# no tiene clases para tratar con números de punto flotante de precisión arbitraria (consulte el software para aritmética de precisión arbitraria ).
C# puede ayudar a las aplicaciones matemáticas con los operadores checked
y unchecked
que permiten habilitar o deshabilitar la verificación en tiempo de ejecución para detectar desbordamiento aritmético para una región de código.
La consulta integrada del lenguaje C# (LINQ) es un conjunto de características diseñadas para trabajar juntas para permitir capacidades de consulta dentro del lenguaje y es una característica distintiva entre C# y Java.
LINQ consta de estas características:
Interoperabilidad nativa | Java | DO# |
---|---|---|
Interoperabilidad entre lenguajes | Sí (con GraalVM , Nashorn , CORBA , JNI o JNA ) [98] | Sí; C# fue diseñado para ello [98] |
Métodos externos/nativos | Sí | Sí |
Organización de reuniones | Se necesita código de pegamento externo | Sí; metadatos controlados |
Punteros y aritmética | No; pero vea sun.misc.Unsafe | Sí |
Tipos nativos | Sí [99] | Sí |
Buffers de tamaño fijo | No | Sí |
Asignación explícita de pila | No | Sí |
Dirección de | No | Sí |
Fijación de objetos (fijar variable a dirección) | No | Sí |
Punteros de función | No | Sí [100] |
Sindicatos | No | Sí [101] |
La función Java Native Interface (JNI) permite que los programas Java invoquen código que no sea Java. Sin embargo, JNI requiere que el código que se invoca siga varias convenciones e impone restricciones sobre los tipos y nombres utilizados. Esto significa que a menudo se necesita una capa de adaptación adicional entre el código heredado y Java. Este código de adaptación debe estar codificado en un lenguaje que no sea Java, a menudo C o C++. Java Native Access (JNA) permite invocar código nativo de manera más sencilla, lo que solo requiere escribir código Java, pero tiene un costo de rendimiento.
Además, las bibliotecas de terceros proporcionan un puente entre el Modelo de objetos componentes de Java (COM), por ejemplo, JACOB ( gratuito ) y J-Integra para COM ( propietario ).
.NET Platform Invoke ( P/Invoke ) ofrece la misma capacidad al permitir llamadas desde C# a lo que Microsoft denomina código no administrado . A través de los atributos de metadatos, el programador puede controlar exactamente cómo se ordenan los parámetros y los resultados , evitando así el código de unión externo que necesita el JNI equivalente en Java. P/Invoke permite un acceso casi completo a las API de procedimiento (como Win32 o POSIX), pero un acceso limitado a las bibliotecas de clases de C++.
Además, .NET Framework también proporciona un puente .NET-COM, que permite el acceso a los componentes COM como si fueran objetos .NET de primera clase.
C# también permite al programador desactivar la comprobación de tipos normal y otras funciones de seguridad del CLR , lo que permite el uso de variables de puntero . Al utilizar esta función, el programador debe marcar el código con la palabra clave unsafe . JNI, P/Invoke y el código "unsafe" son funciones igualmente riesgosas, que exponen posibles agujeros de seguridad e inestabilidad de la aplicación. Una ventaja del código administrado no seguro sobre P/Invoke o JNI es que permite al programador seguir trabajando en el entorno familiar de C# para realizar algunas tareas que de otro modo requerirían llamar a código no administrado. Un ensamblaje (programa o biblioteca) que utiliza código no seguro debe compilarse con un modificador especial y se marcará como tal. Esto permite que los entornos de ejecución tomen precauciones especiales antes de ejecutar código potencialmente dañino.
Java (el lenguaje de programación) está diseñado para ejecutarse en la plataforma Java a través del entorno de ejecución de Java (JRE). La plataforma Java incluye la máquina virtual Java (JVM) y un conjunto común de bibliotecas. El JRE se diseñó originalmente para admitir la ejecución interpretada con la compilación final como opción. La mayoría de los entornos JRE ejecutan programas compilados total o parcialmente, posiblemente con optimización adaptativa . El compilador de Java produce código de bytes de Java . Tras la ejecución, el código de bytes se carga en el entorno de ejecución de Java y se interpreta directamente o se compila en instrucciones de máquina y luego se ejecuta.
C# está diseñado para ejecutarse en Common Language Runtime (CLR). CLR está diseñado para ejecutar código completamente compilado. El compilador de C# produce instrucciones de Common Intermediate Language . Tras la ejecución, el entorno de ejecución carga este código y lo compila en instrucciones de máquina en la arquitectura de destino.
Ejemplo que ilustra cómo copiar texto una línea a la vez de un archivo a otro, usando ambos idiomas.
Java | DO# |
---|---|
importar java.nio.file.* ; clase FileIOTest { public static void main ( String [] args ) lanza una excepción { var líneas = Archivos . readAllLines ( Rutas . get ( "entrada.txt" )); Archivos . write ( Rutas . get ( "salida.txt" ), líneas ); } } | utilizando System.IO ; clase FileIOTest { public static void Main ( string [ ] args ) { var líneas = File.ReadLines ( "input.txt" ); File.WriteAllLines ( " output.txt " , líneas ) ; } } |
Notas sobre la implementación de Java:
| Notas sobre la implementación de C#:
|
C# permite que los tipos definidos por la biblioteca se integren con tipos y operadores existentes mediante el uso de conversiones implícitas/explícitas personalizadas y sobrecarga de operadores, como lo ilustra el siguiente ejemplo:
Java | DO# |
---|---|
var bigNumber = new BigInteger ( "123456789012345678901234567890" ); var respuesta = numero_grande .multiplicar ( número_grande .valorDe ( 42 )) ; var cuadrado = numero_grande .sqrt ( ) ; var suma = numero_grande .suma ( número_grande ) ; | var númerogrande = enterogrande . Analizar ( "123456789012345678901234567890" ); var respuesta = número grande * 42 ; var cuadrado = número grande * número grande ; var suma = número grande + número grande ; |
Java | DO# |
---|---|
// una clase objetivo class Target { public boolean targetMethod ( String arg ) { // hacer algo return true ; } } // uso void doSomething () { // construye un objetivo con el método target var target = new Target (); // capturar la referencia del método Function < String , Boolean > ivk = target :: targetMethod ; // invoca el método referenciado var result = ivk . apply ( "argumentstring" ); } | // una clase objetivo class Target { public bool TargetMethod ( string arg ) { // hacer algo return true ; } } // uso void DoSomething () { // construye un objetivo con el método target var target = new Target (); // capturar el delegado para invocarlo más tarde Func < string , bool > dlg = target . TargetMethod ; // invocar al delegado bool result = dlg ( "argumentstring" ); } |
Java | DO# |
---|---|
Java no tiene esta característica, aunque es posible un efecto similar con la clase Opcional var a = Opcional . de ( 42 ); var b = Opcional . < Entero > vacío (); var c = a . flatMap ( aa -> b . map ( bb -> aa * bb )); | int? a = 42 ; int? b = nulo ; // c recibirá el valor nulo // porque * se levanta y uno de los operandos es nulo int? c = a * b ; |
Este ejemplo ilustra cómo se pueden utilizar Java y C# para crear e invocar una instancia de una clase que se implementa en otro lenguaje de programación. La clase "Deepthought" se implementa utilizando el lenguaje de programación Ruby y representa una calculadora simple que multiplicará dos valores de entrada ( a y b ) cuando se invoque el método Calculate . Además de la forma convencional, Java tiene GraalVM , una máquina virtual capaz de ejecutar cualquier lenguaje de programación implementado.
Java | DO# |
---|---|
Uso de GraalVMContexto poliglota = Contexto .newBuilder () .allowAllAccess ( true ) .build ( ); // Valor de Ruby rubyArray = polyglot . eval ( "ruby" , "[1,2,42,4]" ); int rubyResult = rubyArray . getArrayElement ( 2 ). asInt (); // Valor de Python pythonArray = polyglot . eval ( "python" , "[1,2,42,4]" ); int pythonResult = pythonArray . getArrayElement ( 2 ). asInt (); // Valor de JavaScript jsArray = políglota . evaluación ( "js" , "[1,2,42,4]" ); int jsResult = jsArray . obtenerArrayElement ( 2 ). asInt (); // Valor R rArray = polyglot . eval ( "R" , "c(1,2,42,4)" ); int rResult = rArray . getArrayElement ( 2 ). asInt (); //LLVM (en este caso C, pero podría ser C++, Go, Basic, etc...) Origen source = Source.newBuilder ( " llvm " , new File ( "C_Program.bc" )). build ( ) ; Valor cpart = polyglot.eval ( source ) ; cpart.getMember ( " main " ). execute (); La manera tradicional// Inicializar el motor var invocable = new ScriptEngineManager (). getEngineByName ( "jruby" ); var rubyFile = new FileReader ( "Deepthought.rb" ); engine.eval ( fr ) ; // crea una nueva instancia de la calculadora "Deepthought" var calcClass = engine . eval ( "Deepthought" ); var calc = invocable . evolveMethod ( calcClass , "new" ); // establecer valores de entrada de la calculadora invocable . evolveMethod ( calc , "a=" , 6 ); invocable . evolveMethod ( calc , "b=" , 7 ); // Calcula el resultado var answer = invocable .invokeMethod ( calc , "Calcular" ); | // Inicializar el motorvar runtime = ScriptRuntime . CreateFromConfiguration (); variables globales dinámicas = runtime . Globals ; tiempo de ejecución .ExecuteFile ( " Deepthought.rb" ); // crea una nueva instancia de la calculadora "Deepthought" var calc = globals . Deepthought . @new (); // establecer los valores de entrada de la calculadora calc . a = 6 ; calc . b = 7 ; // Calcular el resultado var respuesta = calc . Calculate (); |
Notas para la implementación de Java:
| Notas para la implementación de C#:
|
Este ejemplo ilustra cómo se puede implementar la secuencia de Fibonacci utilizando los dos lenguajes. La versión de C# aprovecha los métodos generadores de C#. La versión de Java aprovecha la interfaz Stream y las referencias de métodos. Tanto el ejemplo de Java como el de C# utilizan el estilo K&R para el formato de código de clases, métodos y declaraciones.
Java | DO# |
---|---|
// La secuencia de Fibonacci Stream . generate ( new Proveedor < Integer > () { int a = 0 ; int b = 1 ; público Integer obtener () { int temp = a ; a = b ; b = a + temp ; devolver temp ; } }). límite ( 10 ). forEach ( System . out :: println ); | // La secuencia de Fibonacci public IEnumerable < int > Fibonacci () { int a = 0 ; int b = 1 ; mientras ( verdadero ) { rendimiento devuelve a ; ( a , b ) = ( b , a + b ); } } |
// imprimir los primeros 10 números de Fibonacci foreach ( var it in Fibonacci ( ). Take ( 10 )) { Console.WriteLine ( it ) ; } | |
Notas para la versión Java:
Usando un foreachEl mismo ejemplo anterior, pero utilizando un método que devuelve un Iterable para mantener una mayor similitud con el ejemplo de C#. Todo lo que implementa la interfaz iterable se puede iterar en un foreach. Iterable < Integer > fibonacci ( int limit ) { return Stream . generate ( new Proveedor < Integer > () { int a = 0 ; int b = 1 ; público Integer get () { int temp = a ; a = b ; b = a + temp ; return temp ; } }). limit ( limit :: iterador ; } // imprime los primeros 10 números de Fibonacci para ( int it : fibonacci ( 10 ) ) { System . println ( it ) ; } La forma más común de realizar el ejemplo anterior sería usar flujos, no iterables. Esto se podría devolver desde un método como el ejemplo de C#, pero no es necesario y se podría usar directamente simplemente recopilando el flujo. A continuación se muestra un ejemplo que utiliza Streams y la recopilación de la llamada Stream a List en el bloque foreach. var fibonacci = Stream .generate ( new Proveedor < Integer > () { int a = 0 ; int b = 1 ; público Integer get () { int temp = a ; a = b ; b = a + temp ; return temp ; } }) ; // imprime los primeros 10 números de Fibonacci para ( int it : fibonacci . limit ( 10 ). toList ()) { System . out . println ( it ); } Además del recopilador toList en el bloque foreach, es importante destacar que hay más recopiladores para cada tipo de colección. Además, se pueden crear recopiladores personalizados implementando la interfaz Collector o describiendo la implementación como una expresión lambda, en ambos casos pasándola como argumentos al método collect del objeto Stream . En este ejemplo, simplemente se llamaría al método collect en lugar de toList si hubiera algún tipo complejo de objeto por elemento para la colección. Ambos ejemplos también podrían realizarse con IntStream e IntSupplier y evitar el genérico Integer en la implementación de la interfaz Supplier , pero el genérico se usa para preservar una mayor similitud con el ejemplo de C#. Estilo funcionalEl algoritmo anterior se puede escribir de forma aún más consistente, utilizando registro Par ( int a , int b ) {}; Flujo . iterar ( nuevo Par ( 0 , 1 ), p -> nuevo Par ( p . b , p . a + p . b )) . límite ( 10 ) . mapa ( p -> p . a ) . forEach ( Sistema . salida :: println ); | Notas para la versión C#:
|
la extensión de signos congestiona los programas, haciéndolos menos legibles. Por lo tanto, el tipo de byte debe ser sin signo.
{{cite book}}
: CS1 maint: multiple names: authors list (link){{cite book}}
: CS1 maint: multiple names: authors list (link)Rendimiento.
Los enums son rápidos. Casi nunca son un problema de rendimiento. Son simplemente azúcar sintáctico en un tipo como int, que también es rápido. […]
Tipo.
Un enum tiene un tipo subyacente. Cada vez que usamos el enum, estamos usando el tipo subyacente. El enum tiene azúcar sintáctico encima.
En Java, los tipos enumerados son una clase completa, lo que significa que son seguros para los tipos y se pueden ampliar añadiendo métodos, campos o incluso implementando interfaces. Mientras que en C#, un tipo enumerado es simplemente una sintaxis simplificada en torno a un tipo integral (normalmente un int), lo que significa que no se pueden ampliar y no son seguros para los tipos.
Enumeración sind the heimlichen Sieger von Java 1.5. Nach vielen Beteuerungen durch Sun, Enums seien in Java überflüssig und können einfach nachgebildet werden, wurden sie nun doch eingeführt. Die einfachste Möglichkeit einer Enumeration der Jahreszeiten sieht wie folgt aus… Das Schlüsselwort enum steht für eine spezielle Art von Klasse, die eine Enumeration definiert. …
Im Gegensatz zu other Programmiersprachen wie C/C++ y C# kann man ihnen per Gleichheitszeichen keine ganzen Zahlen zuordnen.
La instrucción foreach ejecuta una instrucción o un bloque de instrucciones para cada elemento en una instancia del tipo que implementa la
interfaz
System.Collections.IEnumerable
o
System.Collections.Generic.IEnumerable<T> .
El marco de colecciones de Java no solo tiene métodos que permiten acceder a colecciones no seguras de una manera segura para subprocesos, sino que también contiene versiones seguras para subprocesos de la mayoría de las estructuras de datos. El marco de colecciones de Java tiene varios algoritmos para manipular los elementos dentro de las estructuras de datos, incluidos algoritmos que pueden hacer lo siguiente: encontrar el elemento más grande según algún Comparador, encontrar el elemento más pequeño, encontrar sublistas dentro de una lista, invertir el contenido de una lista, mezclar el contenido de una lista, crear versiones inmutables de una colección, realizar ordenaciones y búsquedas binarias.
El marco de colecciones de C# consta de las clases en los espacios de nombres System. Collections y System.Collections.Generic. El espacio de nombres
Systems.Collections
contiene
interfaces
y clases abstractas que representan tipos de datos abstractos como
IList, IEnumerable, IDictionary, ICollection
y
CollectionBase
que permiten a los desarrolladores manipular estructuras de datos independientemente de cómo se implementan realmente, siempre que las estructuras de datos hereden de los tipos de datos abstractos. El espacio de nombres System.Collections también contiene algunas implementaciones concretas de estructuras de datos como
ArrayList, Stack, Queue, HashTable
y
SortedList
. Las cuatro implementaciones de estructuras de datos concretas permiten obtener contenedores sincronizados para la colección que permiten el acceso de manera segura para subprocesos. El espacio de nombres
System.Collections.Generic
tiene implementaciones genéricas de las estructuras de datos clave en el espacio de nombres System.Collections, incluidas las clases genéricas
List<T>
,
Stack<T>
,
Queue<T>
,
Dictionary<K,T>
y
SortedDictionary<K,T>
.
Nota: A diferencia de C++, C# no permite la sobrecarga de los siguientes operadores;
new, ( ), ||, &&, =
, o cualquier variación de asignaciones compuestas como
+=, -=
, etc. Sin embargo, los operadores de asignación compuesta llamarán a operadores sobrecargados, por ejemplo,
+=
llamaría a
+
sobrecargado .
Hay más de un uso de la palabra clave final para el que C# no tiene un equivalente. Cuando pasas un parámetro a un método en Java y no quieres que el valor de ese parámetro cambie dentro del ámbito de ese método, puedes establecerlo como final de esta manera:
{{cite web}}
: CS1 maint: location (link)Los métodos de extensión son una sintaxis muy agradable. En realidad, no se agregan a la clase, como podemos ver, pero el compilador hace que parezca que sí.
Los métodos de extensión se definen como métodos estáticos, pero se invocan mediante la sintaxis de método de instancia.
Si ninguna parte de una declaración de tipo parcial contiene una declaración de implementación para un método parcial dado, cualquier declaración de expresión que lo invoque simplemente se elimina de la declaración de tipo combinada. Por lo tanto, la expresión de invocación, incluidas las expresiones constituyentes, no tiene efecto en tiempo de ejecución. El método parcial en sí también se elimina y no será miembro de la declaración de tipo combinada. Si existe una declaración de implementación para un método parcial dado, se conservan las invocaciones de los métodos parciales. El método parcial da lugar a una declaración de método similar a la declaración de método parcial de implementación, excepto por lo siguiente: […]
La palabra clave in hace que los argumentos se pasen por referencia. Es como las palabras clave ref o out, excepto que los argumentos in no pueden ser modificados por el método llamado.
En Java, los argumentos de un método se pasan por valor, lo que significa que un método opera sobre copias de los elementos que se le pasan en lugar de sobre los elementos reales. En C#, como en C++ y, en cierto sentido, en C, es posible especificar que los argumentos de un método sean en realidad referencias a los elementos que se pasan al método en lugar de copias. Esta característica es particularmente útil cuando se desea crear un método que devuelva más de un objeto. En Java, intentar devolver múltiples valores desde un método no es compatible y genera anomalías como: un método que intercambia dos números, que ha sido el sello distintivo de las clases de informática de primer año durante años, es imposible de hacer en Java sin recurrir a trucos de codificación.
una diferencia clave entre los atributos de C# y las anotaciones de Java es que se pueden crear metaanotaciones (es decir, anotaciones sobre anotaciones) en Java, pero no se puede hacer lo mismo en C#. Los desarrolladores pueden crear sus propias anotaciones personalizadas creando un tipo de anotación que sea similar a una interfaz, excepto que se utiliza la palabra clave @interface para definirla.
Las operaciones de punto flotante se pueden realizar con mayor precisión que el tipo de resultado de la operación. Por ejemplo, algunas arquitecturas de hardware admiten un tipo de punto flotante "extendido" o "doble largo" con mayor rango y precisión que el tipo doble, y realizan implícitamente todas las operaciones de punto flotante utilizando este tipo de mayor precisión. Solo con un costo excesivo en rendimiento se puede hacer que dichas arquitecturas de hardware realicen operaciones de punto flotante con
menos
precisión y, en lugar de requerir una implementación que pierda tanto rendimiento como precisión, C# permite que se use un tipo de mayor precisión para todas las operaciones de punto flotante. Aparte de ofrecer resultados más precisos, esto rara vez tiene efectos mensurables. Sin embargo, en expresiones de la forma x*y/z, donde la multiplicación produce un resultado que está fuera del rango doble, pero la división posterior devuelve el resultado temporal al rango doble, el hecho de que la expresión se evalúe en un formato de rango superior puede provocar que se produzca un resultado finito en lugar de infinito.
Hay varias formas en que funciona la interoperabilidad entre lenguajes en Java. En primer lugar, está la interfaz nativa de Java (JNI) … Java también tiene la capacidad de interactuar con objetos distribuidos que utilizan la arquitectura de intermediario de solicitud de objetos común (CORBA) a través de Java IDL. … C# y el entorno de ejecución .NET se crearon con la interoperabilidad entre lenguajes sin fisuras como objetivo de diseño.