O que realmente aconteceria se java.lang.String não fosse final?

16

Sou desenvolvedor Java há muito tempo e, finalmente, depois de me formar, tenho tempo para estudá-lo decentemente para fazer o exame de certificação ... Uma coisa que sempre me incomodou é que String é "final". Eu entendo isso quando leio sobre problemas de segurança e coisas relacionadas ... Mas, sério, alguém tem um exemplo verdadeiro disso?

Por exemplo, o que aconteceria se String não fosse final? Como se não estivesse em Ruby. Não recebi nenhuma reclamação da comunidade Ruby ... E estou ciente das StringUtils e classes relacionadas que você precisa implementar ou pesquisar na Web para implementar apenas esse comportamento (4 linhas de código) que você está disposto a.

wleao
fonte
9
Você pode estar interessado nesta questão no Stack Overflow: stackoverflow.com/questions/2068804/why-is-string-final-in-java
Thomas Owens
Da mesma forma, o que aconteceria se java.io.Filenão fosse final. Oh não é. Bugger.
Tom Hawtin - defina
1
O fim da civilização ocidental como a conhecemos.
Tulains Córdova

Respostas:

22

O principal motivo é a velocidade: as finalclasses não podem ser estendidas, o que permitiu ao JIT fazer todos os tipos de otimizações ao manipular strings - nunca é necessário verificar se há métodos substituídos.

Outro motivo é a segurança do encadeamento: os imutáveis ​​são sempre protegidos por encadeamento, porque um encadeamento precisa construí-los completamente antes de serem passados ​​para outra pessoa - e após a construção, eles não podem mais ser alterados.

Além disso, os inventores do tempo de execução Java sempre quiseram errar no lado da segurança. Ser capaz de estender String (algo que costumo fazer no Groovy porque é muito conveniente) pode abrir uma lata inteira de worms se você não souber o que está fazendo.

Aaron Digulla
fonte
2
É uma resposta incorreta. Além da verificação exigida pelas especificações da JVM, o HotSpot usa apenas finalcomo verificação rápida que um método não seja substituído durante a compilação de código.
Tom Hawtin - defina
1
@ TomHawtin-tackline: Referências?
Aaron Digulla
1
Você parece sugerir que imutabilidade e ser final estão de alguma forma relacionados. "Outro motivo é a segurança da linha: os imutáveis ​​são sempre seguros para a linha b ...". A razão pela qual um objeto é imutável ou não não é porque é ou não é final.
Tulains Córdova
1
Além disso, a classe String é imutável, independentemente da finalpalavra-chave na classe. A matriz de caracteres que ele contém é final, tornando-o imutável. Tornar a própria classe final fecha o loop como o @abl menciona, pois uma subclasse mutável não pode ser introduzida.
1
@ Snowman Para ser mais preciso, a matriz de caracteres sendo final apenas torna a referência da matriz imutável, e não o conteúdo da matriz.
Michał Kosmulski
10

Há outra razão pela qual a Stringclasse do Java precisa ser final: é importante para a segurança em alguns cenários. Se a Stringclasse não fosse final, o código a seguir estaria vulnerável a ataques sutis de um chamador 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>";
 }

O ataque: um chamador malicioso pode criar uma subclasse de String,, EvilStringonde EvilString.startsWith()sempre retorna true, mas onde o valor de EvilStringé algo ruim (por exemplo, javascript:alert('xss')). Devido à subclasse, isso evitaria a verificação de segurança. Esse ataque é conhecido como vulnerabilidade de tempo de verificação no tempo de uso (TOCTTOU): entre o momento em que a verificação é concluída (que o URL inicia com http / https) e o momento em que o valor é usado (para construir o snippet html), o valor efetivo pode mudar. Se Stringnão fosse final, os riscos do TOCTTOU seriam generalizados.

Portanto, se Stringnão for final, fica complicado escrever um código seguro se você não puder confiar no seu chamador. Obviamente, essa é exatamente a posição em que as bibliotecas Java estão: elas podem ser invocadas por applets não confiáveis, para que não se atrevam a confiar no chamador. Isso significa que seria excessivamente complicado escrever código de biblioteca seguro, se Stringnão fosse final.

DW
fonte
Obviamente, ainda é possível obter o TOCTTOU vuln usando reflexão para modificar o char[]interior da string, mas isso requer alguma sorte no tempo.
Peter Taylor
2
@ Peter Taylor, é mais complicado que isso. Você só pode usar a reflexão para acessar variáveis ​​privadas se o SecurityManager lhe der permissão. Applets e outros códigos não confiáveis ​​não recebem essa permissão e, portanto, não podem usar a reflexão para modificar o privado char[]dentro da string; portanto, eles não podem explorar a vulnerabilidade TOCTTOU.
DW
Eu sei, mas isso ainda deixa aplicativos e applets assinados - e quantas pessoas realmente se assustam com a caixa de diálogo de aviso de um applet autoassinado?
Peter Taylor
2
@ Peter, esse não é o ponto. O ponto é que escrever bibliotecas Java confiáveis ​​seria muito mais difícil e haveria muito mais vulnerabilidades relatadas nas bibliotecas Java, se Stringnão fosse final. Desde que esse modelo de ameaça seja relevante algumas vezes, torna-se importante se defender. Desde que seja importante para miniaplicativos e outro código não confiável, isso é suficiente para justificar a Stringfinalização, mesmo que não seja necessário para aplicativos confiáveis. (Em qualquer caso, confiável aplicações já pode anular todas as medidas de segurança, independentemente se é final.)
DW
"O ataque: um chamador malicioso pode criar uma subclasse de String, EvilString, onde EvilString.startsWith () sempre retorna verdadeiro ..." - não se o beginWith () for final.
Erik
4

Caso contrário, os aplicativos Java multithread seriam uma bagunça (ainda pior do que realmente é).

IMHO A principal vantagem das seqüências finais (imutáveis) é que elas são inerentemente seguras para threads: elas não requerem sincronização (escrever esse código às vezes é bastante trivial, mas é mais ou menos distante disso). Se eles fossem mutáveis, garanta que coisas como kits de ferramentas do Windows multithread seriam muito difíceis, e essas coisas são estritamente necessárias.

Randolf Rincón Fadul
fonte
3
finalclasses não têm nada a ver com a mutabilidade da classe.
Esta resposta não aborda a questão. -1
FUZxxl
3
Jarrod Roberson: se a classe não fosse final, você poderia criar Strings mutáveis ​​usando herança.
ysdx
@JarrodRoberson finalmente alguém percebe o erro. A resposta aceita também implica iguais finais imutáveis.
Tulains Córdova
1

Outra vantagem de Stringser final é que ele garante resultados previsíveis, assim como a imutabilidade. String, embora seja uma classe, deve ser tratado em alguns cenários, como um tipo de valor (o compilador até suporta String blah = "test"). Portanto, se você criar uma string com um determinado valor, espera que ele tenha um comportamento herdado de qualquer string, semelhante às expectativas que você teria com números inteiros.

Pense sobre isso: e se você subclasse java.lang.Stringe substituiu os métodos equalsou hashCode? De repente, duas "cordas" de teste não podiam mais se igualar.

Imagine o caos se eu pudesse fazer isso:

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

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

alguma outra classe:

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
fonte
1

fundo

Existem três lugares onde final pode aparecer em Java:

A finalização de uma aula evita todas as subclasses da classe. A finalização de um método impede que subclasses do método o substituam. Tornar um campo final evita que seja alterado posteriormente.

Equívocos

Há otimizações que acontecem em torno dos métodos e campos finais.

Um método final facilita a otimização do HotSpot via inlining. No entanto, o HotSpot faz isso mesmo que o método não seja final, pois trabalha com a suposição de que não foi substituído até prova em contrário.Mais sobre isso no SO

Uma variável final pode ser otimizada agressivamente, e mais sobre isso pode ser lido na seção 17.5.3 do JLS . .

No entanto, com esse entendimento, deve-se estar ciente de que nenhuma dessas otimizações é sobre fazer uma classe final. Não há ganho de desempenho ao final da aula.

O aspecto final de uma classe também não tem nada a ver com imutabilidade. Pode-se ter uma classe imutável (como BigInteger ) que não é final ou uma classe que é mutável e final (como StringBuilder ). A decisão sobre se uma classe deve ser final é uma questão de design.

Design final

Strings são um dos tipos de dados mais usados. Eles são encontrados como chaves para mapas, armazenam nomes de usuário e senhas, são o que você lê de um teclado ou de um campo em uma página da web. As cordas estão em toda parte .

Mapas

A primeira coisa a considerar sobre o que aconteceria se você pudesse subclassificar String é perceber que alguém poderia construir uma classe String mutável que, de outra forma, pareceria uma String. Isso atrapalhava o Google Maps em todos os lugares.

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");

Esse é um problema com o uso de uma chave mutável em geral com um mapa, mas o que estou tentando entender é que, de repente, várias coisas sobre a quebra do mapa. A entrada não está mais no ponto certo no mapa. Em um HashMap, o valor do hash mudou (deveria ter) alterado e, portanto, não está mais na entrada correta. No TreeMap, a árvore agora está quebrada porque um dos nós está do lado errado.

Como o uso de uma String para essas chaves é tão comum, esse comportamento deve ser evitado, tornando a String final.

Você pode estar interessado em ler Por que o String é imutável em Java? para saber mais sobre a natureza imutável das Strings.

Cordas nefastas

Existem várias opções nefastas para Strings. Considere se eu criei uma String que sempre retornava verdadeira quando igual era chamado ... e passava isso para uma verificação de senha? Ou fez com que as atribuições para MyString enviassem uma cópia da String para algum endereço de email?

Essas são possibilidades muito reais quando você pode subclassificar String.

Otimizações de Java.lang String

Enquanto antes mencionei que final não faz String mais rápido. No entanto, a classe String (e outras classes em java.lang) fazem uso frequente da proteção de campos e métodos no nível do pacote para permitir que outras java.langclasses possam mexer com os internos, em vez de passar pela API pública do String o tempo todo. Funções como getChars sem verificação de intervalo ou lastIndexOf que é usado por StringBuffer, ou o construtor que compartilha a matriz subjacente (observe que isso é uma coisa do Java 6 que foi alterada devido a problemas de memória).

Se alguém fizesse uma subclasse de String, não seria possível compartilhar essas otimizações (a menos que fizesse parte java.langtambém, mas esse é um pacote selado ).

É mais difícil projetar algo para extensão

Projetar algo para ser extensível é difícil . Isso significa que você precisa expor partes de seus componentes internos para que outra coisa possa ser modificada.

Uma String extensível não poderia ter seu vazamento de memória corrigido. Essas partes precisariam ter sido expostas a subclasses e a alteração desse código significaria que as subclasses iriam quebrar.

O Java se orgulha da compatibilidade com versões anteriores e, ao abrir as classes principais para extensão, perde-se parte dessa capacidade de consertar as coisas, mantendo a computabilidade com subclasses de terceiros.

O Checkstyle possui uma regra que ela aplica (o que realmente me frustra ao escrever código interno) chamada "DesignForExtension", que impõe que todas as classes sejam:

  • Abstrato
  • Final
  • Implementação vazia

O racional para o qual é:

Esse estilo de design da API protege as superclasses contra quebras por subclasses. A desvantagem é que as subclasses são limitadas em sua flexibilidade, em particular elas não podem impedir a execução de código na superclasse, mas isso também significa que as subclasses não podem corromper o estado da superclasse ao esquecer de chamar o método super.

Permitir que as classes de implementação sejam estendidas significa que as subclasses podem corromper o estado da classe em que se baseia e torná-lo de modo que várias garantias que a superclasse fornece sejam inválidas. Para algo tão complexo como String, é quase uma certeza que a mudança de parte dela vai quebrar alguma coisa.

Desenvolvedor hurbis

É parte de ser um desenvolvedor. Considere a probabilidade de que cada desenvolvedor crie sua própria subclasse String com sua própria coleção de utilitários . Mas agora essas subclasses não podem ser livremente atribuídas uma à outra.

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

Este caminho leva à loucura. Transmitindo para String em todo o lugar e verificando se a String é uma instância do seu classe String ou não, criando uma nova String com base nessa e ... apenas, não. Não.

Tenho certeza que você pode escrever uma classe bom string ... mas deixar de escrever várias implementações de cadeia para essas pessoas loucas que escrevem C ++ e tem que lidar com std::stringe char*e algo de impulso e sString , e todo o resto .

Java String magic

Existem várias coisas mágicas que Java faz com Strings. Isso facilita o trabalho do programador, mas introduz algumas inconsistências no idioma. Permitir subclasses em String exigiria uma reflexão muito significativa sobre como lidar com essas coisas mágicas:

  • Literais de sequência (JLS 3.10.5 )

    Ter um código que permita:

    String foo = "foo";

    Isso não deve ser confundido com o boxe de tipos numéricos como Inteiro. Você não pode fazer 1.toString(), mas você pode fazer "foo".concat(bar).

  • O +operador (JLS 15.18.1 )

    Nenhum outro tipo de referência em Java permite que um operador seja usado nele. A String é especial. O operador de concatenação também trabalha no nível do compilador de modo que "foo" + "bar"se torna "foobar"quando ele é compilado em vez de em tempo de execução.

  • Conversão de Cadeia de Caracteres (JLS 5.1.11 )

    Todos os objetos podem ser convertidos em Strings usando-os em um contexto de String.

  • Internação de strings ( JavaDoc )

    A classe String tem acesso ao pool de Strings que permite ter representações canônicas do objeto que é preenchido no tipo de compilação com os literais String.

Permitir uma subclasse de String significaria que esses bits com a String que facilitam a programação se tornariam muito difíceis ou impossíveis de executar quando outros tipos de String forem possíveis.

Comunidade
fonte
0

Se String não fosse final, cada baú de ferramentas de programador conteria seu próprio "String com apenas alguns métodos auxiliares agradáveis". Cada um deles seria incompatível com todos os outros baús de ferramentas.

Eu considerei uma restrição tola. Hoje estou convencido de que foi uma decisão muito sólida. Mantém Strings para serem Strings.


fonte
finalem Java não é a mesma coisa que finalem c #. Nesse contexto, é a mesma coisa que c # 's readonly. Veja stackoverflow.com/questions/1327544/…
Arseni Mourzenko
9
@MainMa: Na verdade, neste contexto, parece que é o mesmo que o C # está selado, como em public final class String.
configurator