Alguém pode me explicar o que o raciocínio por trás passando por “valor” e não por “referência” em Java é?
-
21-08-2019 - |
Pergunta
Eu sou bastante novo para Java (escrito outras coisas por muitos anos) e menos que eu estou faltando alguma coisa (e eu estou feliz por estar errado aqui) O seguinte é uma falha fatal ...
String foo = new String();
thisDoesntWork(foo);
System.out.println(foo);//this prints nothing
public static void thisDoesntWork(String foo){
foo = "howdy";
}
Agora, eu estou bem ciente do (bastante mal formulada) conceito que em java tudo é passado por "valor" e não "de referência", mas String é um objeto e tem todos os tipos de sinos e assobios, assim, seria de esperar que ao contrário de um int um usuário seria capaz de operar na coisa que é passado para o método (e não ser preso com o valor definido pelo sobrecarregado =).
Alguém pode me explicar o que o raciocínio por trás dessa escolha de design foi? Como eu disse, eu não estou olhando para estar aqui, e talvez eu estou faltando algo óbvio?
Solução
Este discurso explica isso melhor do que eu jamais poderia sequer tentar:
Em Java, primitivos são passados ??por valor. No entanto, os objetos não são passado por referência. A afirmação correta seria referências de objeto são passados ??por valor.
Outras dicas
Quando você passar "foo", você está passando o referência para "foo" como um valor para ThisDoesntWork (). Isso significa que quando você faz a atribuição para dentro "foo" do seu método, você está apenas definindo uma variável local (foo) de referência a ser uma referência para a sua nova cadeia.
Outra coisa a ter em mente quando se pensa em como as strings se comportam em Java é que strings são imutáveis. Ele funciona da mesma forma em C #, e para algumas boas razões:
- Segurança : Ninguém pode jam dados em sua seqüência e causar um erro de buffer overflow se ninguém pode modificá-lo
- velocidade : Se você pode ter certeza que suas cordas são imutáveis, você sabe o seu tamanho é sempre o mesmo e você não sempre tem que fazer um movimento da estrutura de dados na memória quando você manipulá-la. Você (o designer da linguagem) também não precisa se preocupar com a implementação do String como uma lista ligada lento, também. Esta faca de dois gumes, no entanto. Anexando cordas apenas usando o operador + pode ser caro memory-sábio, e você terá que usar um objeto StringBuilder para fazer isso de uma maneira de alta performance, memória-eficiente.
Agora, em sua grande questão. Por que são objetos passaram por este caminho? Bem, se Java passou sua seqüência como o que você tradicionalmente chamar "por valor", ele teria que realmente copiar toda a cadeia antes de passá-lo para a sua função. Isso é bastante lento. Se ele passou a cadeia por referência e deixá-lo mudá-lo (como C faz), você teria os problemas que acabei de listar.
Uma vez que a minha resposta original era "Por que isso aconteceu" e não "Por que era a língua projetada assim aconteceu:" Eu vou dar a esta outra vez.
Para simplificar as coisas, vou livrar-se da chamada de método e mostrar o que está acontecendo de outra maneira.
String a = "hello";
String b = a;
String b = "howdy"
System.out.print(a) //prints hello
Para obter a última declaração para imprimir "Olá", b tem que apontam para o mesmo "buraco" na memória que a aponta para (um ponteiro). Isto é o que você quiser, quando quiser passagem por referência. Há um par de razões Java decidiu não ir nesta direção:
-
Os ponteiros são confusas Os designers de Java tentou remover algumas das mais confusas coisas sobre outras línguas. Os ponteiros são um dos mais mal compreendida e impropriamente construções de C / C ++ utilizada juntamente com a sobrecarga de operador.
-
Os ponteiros são os riscos de segurança Ponteiros causar muitos problemas de segurança quando mal utilizado. Um programa malicioso cessionários algo a que parte da memória, então o que você pensou que era o seu objeto é realmente outra pessoa. (Java já se livrou do maior problema de segurança, buffer overflows, com matrizes verificados)
-
Abstraction Vazamento Quando você começar a lidar com "o que está na memória e onde" exatamente, sua abstração torna-se menos de uma abstração. Enquanto vazamento abstração quase certamente se arrasta em uma língua, os designers não queria cozê-lo diretamente.
-
Objetos são tudo o que importa ??em> Em Java, tudo é um objeto, não o espaço um objeto ocupa. Adicionando ponteiros iria tornar o espaço um objeto ocupa importantant, embora .......
Você poderia imitar o que você quer através da criação de um objeto "Hole". Você poderia até usar os genéricos para torná-lo tipo seguro. Por exemplo:
public class Hole<T> {
private T objectInHole;
public void putInHole(T object) {
this.objectInHole = object;
}
public T getOutOfHole() {
return objectInHole;
}
public String toString() {
return objectInHole.toString();
}
.....equals, hashCode, etc.
}
Hole<String> foo = new Hole<String)();
foo.putInHole(new String());
System.out.println(foo); //this prints nothing
thisWorks(foo);
System.out.println(foo);//this prints howdy
public static void thisWorks(Hole<String> foo){
foo.putInHole("howdy");
}
A sua questão como solicitado não tem realmente a ver com a passagem por valor, passando por referência, ou o fato de que as cordas são imutáveis ?? (como outros já referiram).
Dentro do método, você realmente criar uma variável local (vou chamá que um "localFoo") que aponta para a mesma referência como a variável original ( "originalFoo").
Quando você atribui "Olá" para localFoo, você não alterar o local onde originalFoo está apontando.
Se você fez algo como:
String a = "";
String b = a;
String b = "howdy"?
Você esperaria:
System.out.print(a)
para imprimir "Olá"? Ele imprime "".
Você não pode mudar o que originalFoo pontos para mudando o que pontos localFoo para. Você pode modificar o objeto que ambos apontam para (se não fosse imutável). Por exemplo,
List foo = new ArrayList();
System.out.println(foo.size());//this prints 0
thisDoesntWork(foo);
System.out.println(foo.size());//this prints 1
public static void thisDoesntWork(List foo){
foo.add(new Object);
}
Em java todas as variáveis ??passadas são realmente repassados ??por valor- mesmo objetos. Todas as variáveis ??passadas para um método são na verdade cópias do valor original. No caso do seu exemplo seqüência o ponteiro originais. (Sua realmente uma referência - mas para evitar confusão doente usar uma palavra diferente) é copiado para uma nova variável que se torna o parâmetro para o método
Seria uma dor se foi tudo por referência. Seria necessário fazer cópias privadas em todo o lugar que com certeza gostaria de ser uma dor real. Todo mundo sabe que o uso de imutabilidade para tipos de valor etc faz com que seus programas infinitamente mais simples e escalável.
Alguns benefícios incluem: - Não há necessidade de fazer cópias defensivas. - Threadsafe -. Não precisa se preocupar sobre o bloqueio apenas no caso de alguém quiser mudar o objeto
O problema é que você está instanciar um tipo de referência Java. Então você passar esse tipo de referência a um método estático, e atribuí-la a uma variável com escopo localmente.
Não tem nada a ver com a imutabilidade. Exatamente a mesma coisa teria acontecido para um tipo de referência mutáveis.
Se quisermos fazer uma áspera C e assembler analogia:
void Main()
{
// stack memory address of message is 0x8001. memory address of Hello is 0x0001.
string message = "Hello";
// assembly equivalent of: message = "Hello";
// [0x8001] = 0x0001
// message's stack memory address
printf("%d", &message); // 0x8001
printf("%d", message); // memory pointed to of message(0x8001): 0x0001
PassStringByValue(message); // pass the pointer pointed to of message. 0x0001, not 0x8001
printf("%d", message); // memory pointed to of message(0x8001): 0x0001. still the same
// message's stack memory address doesn't change
printf("%d", &message); // 0x8001
}
void PassStringByValue(string foo)
{
printf("%d", &foo); // &foo contains foo's *stack* address (0x4001)
// foo(0x4001) contains the memory pointed to of message, 0x0001
printf("%d", foo); // 0x0001
// World is in memory address 0x0002
foo = "World"; // on foo's memory address (0x4001), change the memory it pointed to, 0x0002
// assembly equivalent of: foo = "World":
// [0x4001] = 0x0002
// print the new memory pointed by foo
printf("%d", foo); // 0x0002
// Conclusion: Not in any way 0x8001 was involved in this function. Hence you cannot change the Main's message value.
// foo = "World" is same as [0x4001] = 0x0002
}
void Main()
{
// stack memory address of message is 0x8001. memory address of Hello is 0x0001.
string message = "Hello";
// assembly equivalent of: message = "Hello";
// [0x8001] = 0x0001
// message's stack memory address
printf("%d", &message); // 0x8001
printf("%d", message); // memory pointed to of message(0x8001): 0x0001
PassStringByRef(ref message); // pass the stack memory address of message. 0x8001, not 0x0001
printf("%d", message); // memory pointed to of message(0x8001): 0x0002. was changed
// message's stack memory address doesn't change
printf("%d", &message); // 0x8001
}
void PassStringByRef(ref string foo)
{
printf("%d", &foo); // &foo contains foo's *stack* address (0x4001)
// foo(0x4001) contains the address of message(0x8001)
printf("%d", foo); // 0x8001
// World is in memory address 0x0002
foo = "World"; // on message's memory address (0x8001), change the memory it pointed to, 0x0002
// assembly equivalent of: foo = "World":
// [0x8001] = 0x0002;
// print the new memory pointed to of message
printf("%d", foo); // 0x0002
// Conclusion: 0x8001 was involved in this function. Hence you can change the Main's message value.
// foo = "World" is same as [0x8001] = 0x0002
}
Uma possível razão por que tudo é passado por valor em Java, seus pais designer da linguagem quer simplificar a linguagem e fazer tudo feito de forma OOP.
Eles sim teria de projetar um inteiro swapper usando objetos que eles fornecem um suporte de primeira classe para o by-referência de passagem, o mesmo para delegado (Gosling sente icky com ponteiro para função, ele prefere empinar essa funcionalidade aos objetos) e enum.
Eles excesso de simplificar (tudo é objeto) a linguagem em detrimento de não ter suporte de primeira classe para a maioria das construções de linguagem, por exemplo, passagem por referência, os delegados, enum, propriedades vem à mente.
Você tem certeza que imprime nulo? Acho que vai ser apenas em branco como quando inicializado a variável foo você forneceu cadeia vazia.
A atribuição de foo na thisDoesntWork método não está a mudar a referência da variável foo definida na classe de modo a foo na System.out.printlnl (foo) ainda irá apontar para o velho objecto cadeia vazia.
Dave, você tem que me perdoar (bem, acho que você não "tem que", mas eu prefiro que você fez), mas essa explicação não é muito convincente. Os ganhos de segurança são bastante mínimo desde quem precisa alterar o valor da corda vai encontrar uma maneira de fazê-lo com alguma solução feio. E velocidade ?! Você mesmo (muito corretamente) afirmam que todo o negócio com o + é extremamente caro.
O resto vocês, por favor, entenda que eu entendo como ele funciona, eu estou perguntando por que ele funciona dessa maneira ... por favor pare de explicar a diferença entre as metodologias.
(e eu sinceramente não estou à procura de qualquer tipo de briga aqui, btw, eu só não vejo como esta foi uma decisão racional).
@Axelle
Companheiro você realmente sabe a diferença entre passando por valor e por referência?
Em java mesmo referências são passados ??por valor. Quando você passar uma referência a um objeto que você está recebendo uma cópia do ponteiro de referência na segunda variável. Tahts porque a segunda variável podem ser alterados sem afetar o primeiro.
É porque, ele cria uma variável local dentro do método. o que seria uma maneira fácil (o que eu tenho certeza que iria trabalhar) seria:
String foo = new String();
thisDoesntWork(foo);
System.out.println(foo); //this prints nothing
public static void thisDoesntWork(String foo) {
this.foo = foo; //this makes the local variable go to the main variable
foo = "howdy";
}
Se você pensar em um objeto como apenas os campos no objeto, em seguida, os objetos são passados ??por referência em Java porque um método pode modificar os campos de parâmetro e um visitante pode observar a modificação. No entanto, se você também pensa de um objeto, uma vez que a identidade então os objetos são passados ??por valor porque um método não pode alterar a identidade de um parâmetro de uma forma que o chamador pode observar. Então, eu diria Java é passar por valor.
Isto porque dentro "thisDoesntWork", você está efetivamente destruindo o valor local de foo. Se você quiser passar por referência, desta forma, pode sempre encapsular a string dentro de outro objeto, digamos, em um array.
class Test {
public static void main(String[] args) {
String [] fooArray = new String[1];
fooArray[0] = new String("foo");
System.out.println("main: " + fooArray[0]);
thisWorks(fooArray);
System.out.println("main: " + fooArray[0]);
}
public static void thisWorks(String [] foo){
System.out.println("thisWorks: " + foo[0]);
foo[0] = "howdy";
System.out.println("thisWorks: " + foo[0]);
}
}
Os resultados no seguinte resultado:
main: foo
thisWorks: foo
thisWorks: howdy
main: howdy
Referência digitado argumentos são passados ??como referências a objetos em si (não menções a outros variáveis ?? que se referem a objetos). Você pode chamar métodos no objeto que foi passado. No entanto, em sua amostra de código:
public static void thisDoesntWork(String foo){
foo = "howdy";
}
só está armazenando uma referência ao "howdy"
corda em uma variável que é local para o método. Essa variável local (foo
) foi inicializado com o valor de foo
do chamador quando o método foi chamado, mas não tem nenhuma referência à variável do chamador em si. Após a inicialização:
caller data method
------ ------ ------
(foo) --> "" <-- (foo)
Após a atribuição em seu método:
caller data method
------ ------ ------
(foo) --> ""
"hello" <-- (foo)
Você tem mais problemas lá: casos String
são imutáveis ??(por design, para a segurança) para que você não pode modificar o seu valor
Se você realmente quer que seu método para fornecer um valor inicial para a cadeia (ou pelo qualquer momento em sua vida, para que o assunto), em seguida, ter o seu método retorno um valor String
que você atribui a variável do chamador no ponto da chamada. Algo como este, por exemplo:
String foo = thisWorks();
System.out.println(foo);//this prints the value assigned to foo in initialization
public static String thisWorks(){
return "howdy";
}
Vai fazer o realmente grande tutorial no site sóis.
Você parece não entender variáveis ??a diferença escopos pode ser. "Foo" é local para o seu método. Nada fora desse método pode mudar o que "foo" aponta também. O "foo" a ser referido o seu método é um campo completamente diferente - é um campo estático em sua classe delimitador.
Escopo é especialmente importante porque você não quer que tudo seja visível para todo o resto do seu sistema.