¿Qué pasaría realmente si java.lang.String no fuera definitivo?

16

Soy un desarrollador de Java desde hace mucho tiempo y, finalmente, después de especializarme, tengo tiempo para estudiarlo decentemente para tomar el examen de certificación ... Una cosa que siempre me ha molestado es que String sea "final". Lo entiendo cuando leo sobre los problemas de seguridad y cosas relacionadas ... Pero, en serio, ¿alguien tiene un verdadero ejemplo de eso?

Por ejemplo, ¿qué pasaría si String no fuera definitivo? Como si no estuviera en Ruby. No he escuchado ninguna queja de la comunidad de Ruby ... Y estoy al tanto de StringUtils y las clases relacionadas que tiene que implementar usted mismo o buscar en la web para implementar ese comportamiento (4 líneas de código) Estás dispuesto a hacerlo.

wleao
fuente
9
Puede que le interese esta pregunta sobre Stack Overflow: stackoverflow.com/questions/2068804/why-is-string-final-in-java
Thomas Owens
Del mismo modo, lo que sucedería si java.io.Fileno fuera así final. Oh no lo es. Tío.
Tom Hawtin - tackline
1
El fin de la civilización occidental tal como la conocemos.
Tulains Córdova

Respuestas:

22

La razón principal es la velocidad: las finalclases no se pueden extender, lo que permitió que el JIT hiciera todo tipo de optimizaciones al manejar cadenas; nunca es necesario verificar los métodos anulados.

Otra razón es la seguridad de los subprocesos: los inmutables siempre son seguros para los subprocesos porque un subproceso debe construirlos por completo antes de que se puedan pasar a otra persona, y después de la construcción, ya no se pueden cambiar.

Además, los inventores del tiempo de ejecución de Java siempre quisieron errar por el lado de la seguridad. Ser capaz de extender String (algo que suelo hacer en Groovy porque es muy conveniente) puede abrir una lata entera de gusanos si no sabes lo que estás haciendo.

Aaron Digulla
fuente
2
Es una respuesta incorrecta. Aparte de la verificación requerida por la especificación JVM, HotSpot solo usa finalcomo una comprobación rápida de que un método no se anula al compilar código.
Tom Hawtin - tackline
1
@ TomHawtin-tackline: ¿referencias?
Aaron Digulla
1
Parece implicar que la inmutabilidad y el ser final están de alguna manera relacionados. "Otra razón es la seguridad del hilo: los inmutables siempre son seguros para el hilo b ...". La razón por la que un objeto es inmutable o no no es porque son o no son finales.
Tulains Córdova
1
Además, la clase String es inmutable independientemente de la finalpalabra clave en la clase. La matriz de caracteres que contiene es final, por lo que es inmutable. Hacer que la clase en sí sea final cierra el ciclo como @abl menciona ya que no se puede introducir una subclase mutable.
1
@Snowman Para ser precisos, el conjunto de caracteres que es final solo hace que la referencia del conjunto sea inmutable, no el contenido del conjunto.
Michał Kosmulski
10

Hay otra razón por la cual la Stringclase de Java debe ser final: es importante para la seguridad en algunos escenarios. Si la Stringclase no fuera final, el siguiente código sería vulnerable a ataques sutiles por parte de un llamador malicioso:

 public String makeSafeLink(String url, String text) {
     if (!url.startsWith("http:") && !url.startsWith("https:"))
         throw SecurityException("only http/https URLs are allowed");
     return "<a href=\"" + escape(url) + "\">" + escape(text) + "</a>";
 }

El ataque: un llamador malicioso podría crear una subclase de String, EvilStringdonde EvilString.startsWith()siempre devuelve verdadero pero donde el valor de EvilStringes algo malvado (por ejemplo, javascript:alert('xss')). Debido a la subclasificación, esto evadiría el control de seguridad. Este ataque se conoce como vulnerabilidad de tiempo de verificación a tiempo de uso (TOCTTOU): entre el momento en que se realiza el control (que la url comienza con http / https) y el momento en que se usa el valor (para construir el fragmento html), el valor efectivo puede cambiar. Si Stringno fuera definitivo, los riesgos de TOCTTOU serían generalizados.

Entonces, si Stringno es definitivo, se vuelve difícil escribir código seguro si no puede confiar en su interlocutor. Por supuesto, esa es exactamente la posición en la que se encuentran las bibliotecas de Java: pueden ser invocadas por applets no confiables, por lo que no se atreven a confiar en la persona que llama. Esto significa que sería irrazonablemente difícil escribir un código de biblioteca seguro, si Stringno fuera definitivo.

DW
fuente
Por supuesto, todavía es posible lograr que TOCTTOU vuln use la reflexión para modificar el char[]interior de la cadena, pero requiere algo de suerte en el tiempo.
Peter Taylor
2
@ Peter Taylor, es más complicado que eso. Solo puede usar la reflexión para acceder a variables privadas si SecurityManager le da permiso para hacerlo. Los applets y otros códigos que no son de confianza no tienen este permiso y, por lo tanto, no pueden usar la reflexión para modificar lo privado char[]dentro de la cadena; por lo tanto, no pueden explotar la vulnerabilidad TOCTTOU.
DW
Lo sé, pero eso todavía deja aplicaciones y applets firmados, ¿y cuántas personas realmente se asustan con el cuadro de diálogo de advertencia para un applet autofirmado?
Peter Taylor
2
@ Peter, ese no es el punto. El punto es que escribir bibliotecas Java confiables sería mucho más difícil, y habría muchas más vulnerabilidades reportadas en las bibliotecas Java, si Stringno fuera definitivo. Siempre que este modelo de amenaza sea relevante, será importante defenderse contra él. Siempre y cuando sea importante para los applets y otros códigos que no sean de confianza, podría decirse que es suficiente para justificar la finalización String, incluso si no es necesario para aplicaciones confiables. (En cualquier caso, las aplicaciones confiables ya pueden eludir todas las medidas de seguridad, independientemente de si es definitiva).
DW
"El ataque: una persona que llama maliciosa podría crear una subclase de String, EvilString, donde EvilString.startsWith () siempre devuelve verdadero ...", no si comiences con () es final.
Erik
4

De lo contrario, las aplicaciones Java multiproceso serían un desastre (incluso peor de lo que realmente es).

En mi humilde opinión, la principal ventaja de las cadenas finales (inmutables) es que son inherentemente seguras para subprocesos: no requieren sincronización (escribir ese código a veces es bastante trivial pero está más que menos lejos de eso). Si fueran mutables, garantizar que cosas como los kits de herramientas de Windows multiproceso serían muy difíciles, y esas cosas son estrictamente necesarias.

Randolf Rincón Fadul
fuente
3
finalLas clases no tienen nada que ver con la mutabilidad de la clase.
Esta respuesta no responde a la pregunta. -1
FUZxxl
3
Jarrod Roberson: si la clase no era final, podría crear cadenas mutables utilizando la herencia.
ysdx
@JarrodRoberson finalmente alguien nota el error. La respuesta aceptada también implica final igual a inmutable.
Tulains Córdova
1

Otra ventaja de Stringser final es que garantiza resultados predecibles, al igual que la inmutabilidad. String, aunque es una clase, debe tratarse en algunos escenarios, como un tipo de valor (el compilador incluso lo admite a través de String blah = "test"). Por lo tanto, si crea una cadena con un cierto valor, espera que tenga cierto comportamiento heredado de cualquier cadena, similar a las expectativas que tendría con los enteros.

Piense en esto: ¿Qué pasa si subclasifica java.lang.Stringy sobrepasa los métodos equalso hashCode? De repente, la "prueba" de dos cadenas ya no podía igualarse.

Imagina el caos si pudiera hacer esto:

public class Password extends String {
    public Password(String text) {
        super(text);
    }

    public boolean equals(Object o) {
        return true;
    }
}

alguna otra clase:

public boolean isRightPassword(String suppliedString) {
    return suppliedString != null && suppliedString.equals("secret");
}

public void login(String username, String password) {
    return isRightUsername(username) && isRightPassword(password);
}

public void loginTest() {
    boolean success = login("testuser", new Password("wrongpassword"));
    // success is true, despite the password being wrong
}
Brandon
fuente
1

Antecedentes

Hay tres lugares donde el final puede aparecer en Java:

Hacer una clase final evita todas las subclases de la clase. Hacer que un método sea final evita que las subclases del método lo anulen. Hacer un campo final evita que se cambie más tarde.

Conceptos erróneos

Hay optimizaciones que ocurren alrededor de los métodos y campos finales.

Un método final facilita la optimización de HotSpot a través de la inserción. Sin embargo, HotSpot hace esto incluso si el método no es definitivo, ya que funciona bajo el supuesto de que no se ha anulado hasta que se demuestre lo contrario. Más sobre esto en SO

Una variable final se puede optimizar agresivamente, y se puede leer más sobre eso en la sección 17.5.3 de JLS .

Sin embargo, con esa comprensión, uno debe ser consciente de que ninguna de estas optimizaciones se trata de hacer una clase final. No hay ganancia de rendimiento al hacer una clase final.

El aspecto final de una clase tampoco tiene nada que ver con la inmutabilidad. Uno puede tener una clase inmutable (como BigInteger ) que no sea final, o una clase que sea mutable y final (como StringBuilder ). La decisión sobre si una clase debe ser final es una cuestión de diseño.

Diseño final

Las cadenas son uno de los tipos de datos más utilizados. Se encuentran como claves para los mapas, almacenan nombres de usuario y contraseñas, son lo que se lee desde un teclado o un campo en una página web. Las cuerdas están en todas partes .

Mapas

Lo primero que debe tener en cuenta de lo que sucedería si pudiera subclasificar String es darse cuenta de que alguien podría construir una clase de cadena mutable que, de lo contrario, parecería ser una cadena. Esto estropearía Maps en todas partes.

Considere este código hipotético:

Map t = new TreeMap<String, Integer>();
Map h = new HashMap<String, Integer>();
MyString one = new MyString("one");
MyString two = new MyString("two");

t.put(one, 1); h.put(one, 1);
t.put(two, 2); h.put(two, 2);

one.prepend("z");

Este es un problema con el uso de una clave mutable en general con un Mapa, pero lo que intento encontrar allí es que de repente hay varias cosas sobre la ruptura del Mapa. La entrada ya no está en el lugar correcto en el mapa. En un HashMap, el valor hash ha (debería haber) cambiado y, por lo tanto, ya no está en la entrada correcta. En TreeMap, el árbol ahora está roto porque uno de los nodos está en el lado equivocado.

Dado que el uso de una cadena para estas claves es muy común, este comportamiento debe evitarse haciendo que la cadena sea final.

Te puede interesar leer ¿Por qué String es inmutable en Java? para más información sobre la naturaleza inmutable de Strings.

Cuerdas nefastas

Hay varias opciones nefastas para las cadenas. Considere si hice una Cadena que siempre devuelve verdadero cuando se llamó a igual ... y la pasé a una verificación de contraseña. ¿O para que las asignaciones a MyString envíen una copia de la cadena a alguna dirección de correo electrónico?

Estas son posibilidades muy reales cuando tienes la capacidad de subclasificar String.

Optimizaciones de cadenas Java.lang

Mientras que antes mencioné que la final no hace que String sea más rápido. Sin embargo, la clase String (y otras clases en java.lang) hacen uso frecuente de la protección a nivel de paquete de campos y métodos para permitir que otras java.langclases puedan jugar con los elementos internos en lugar de pasar por la API pública para String todo el tiempo. Funciones como getChars sin comprobación de rango o lastIndexOf que utiliza StringBuffer, o el constructor que comparte la matriz subyacente (tenga en cuenta que eso es algo de Java 6 que se modificó debido a problemas de memoria).

Si alguien hiciera una subclase de String, no podría compartir esas optimizaciones (a menos que también fuera parte de él java.lang, pero ese es un paquete sellado ).

Es más difícil diseñar algo para la extensión

Diseñar algo para que sea extensible es difícil . Significa que debe exponer partes de sus componentes internos para que otra cosa pueda modificar.

Una cadena extensible no podría haber reparado su pérdida de memoria . Esas partes de él tendrían que haber estado expuestas a subclases y cambiar ese código significaría que las subclases se romperían.

Java se enorgullece de su compatibilidad con versiones anteriores y al abrir las clases principales a la extensión, uno pierde parte de esa capacidad para arreglar las cosas y al mismo tiempo mantiene la computabilidad con subclases de terceros.

Checkstyle tiene una regla que impone (que realmente me frustra al escribir código interno) llamada "DesignForExtension" que exige que cada clase sea:

  • Resumen
  • Final
  • Implementación vacía

El racional para el cual es:

Este estilo de diseño de API protege las superclases de las subclases. La desventaja es que las subclases tienen una flexibilidad limitada, en particular no pueden evitar la ejecución de código en la superclase, pero eso también significa que las subclases no pueden corromper el estado de la superclase olvidando llamar al método super.

Permitir que las clases de implementación se extiendan significa que las subclases posiblemente pueden corromper el estado de la clase en la que se basa y hacer que las diversas garantías que brinda la superclase no sean válidas. Para algo tan complejo como la cadena, es casi una certeza que el cambio de parte de ella va a romper algo.

Desarrollador hurbis

Es parte de ser un desarrollador. Considere la probabilidad de que cada desarrollador cree su propia subclase de String con su propia colección de utilidades . Pero ahora estas subclases no pueden asignarse libremente entre sí.

WleaoString foo = new WleaoString("foo");
MichaelTString bar = foo; // This doesn't work.

Este camino lleva a la locura. Lanzando a String por todas partes y verificando si String es una instancia de su clase String o si no, creando una nueva String basada en esa y ... simplemente, no. No lo hagas

Estoy seguro de que puede escribir una clase String bueno ... pero dejo de escribir múltiples implementaciones de cuerda a esos locos que escriben en C ++ y tienen que tratar con std::stringy char*y algo de impulso y sString , y todo el resto .

Java String magic

Hay varias cosas mágicas que Java hace con Strings. Esto hace que sea más fácil para un programador tratar, pero introduce algunas inconsistencias en el lenguaje. Permitir subclases en String requeriría un pensamiento muy significativo sobre cómo lidiar con estas cosas mágicas:

  • Literales de cadena (JLS 3.10.5 )

    Tener un código que le permita a uno hacer:

    String foo = "foo";

    Esto no debe confundirse con el boxeo de los tipos numéricos como Integer. No puedes hacer 1.toString(), pero puedes hacer "foo".concat(bar).

  • El +operador (JLS 15.18.1 )

    Ningún otro tipo de referencia en Java permite que se use un operador en él. La cadena es especial. El operador de concatenación de cadenas también funciona en el nivel del compilador, por lo que se "foo" + "bar"convierte "foobar"cuando se compila en lugar de en tiempo de ejecución.

  • Conversión de cadenas (JLS 5.1.11 )

    Todos los objetos se pueden convertir en cadenas simplemente usándolos en un contexto de cadena.

  • Cadena internar ( JavaDoc )

    La clase String tiene acceso al grupo de Strings que le permite tener representaciones canónicas del objeto que se rellena en el tipo de compilación con los literales de String.

Permitir una subclase de Cadena significaría que estos bits con la Cadena que facilitan la programación se volverán muy difíciles o imposibles de hacer cuando otros tipos de Cadena sean posibles.

Comunidad
fuente
0

Si String no fuera definitivo, cada cofre de herramientas del programador contendría su propio "String con solo unos pocos métodos de ayuda". Cada uno de estos sería incompatible con todos los demás cofres de herramientas.

Lo consideré una restricción tonta. Hoy estoy convencido de que fue una decisión muy acertada. Mantiene las cadenas para ser cadenas.


fuente
finalen Java no es lo mismo que finalen C #. En este contexto, es lo mismo que C # 's readonly. Ver stackoverflow.com/questions/1327544/…
Arseni Mourzenko
9
@MainMa: En realidad, en este contexto parece que es lo mismo que C # 's sellado, como en public final class String.
configurador el