有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java什么是不可变的?

这可能是有史以来问过的最愚蠢的问题,但我认为对于一个Java新手来说,这是相当令人困惑的

  1. 有人能澄清什么是不可变的吗
  2. 为什么String是不可变的
  3. 不可变对象的优点/缺点是什么
  4. 为什么像StringBuilder这样的可变对象应该优先于字符串和反之亦然

一个很好的例子(在Java中)将非常感谢


共 (6) 个答案

  1. # 1 楼答案

    实际上,如果您使用上面建议的wikipedia定义,字符串不是不可变的

    字符串的状态在构造后确实会更改。看看hashcode()方法。字符串在本地字段中缓存hashcode值,但直到第一次调用hashcode()时才计算它。这种对hashcode的惰性计算将字符串作为状态发生变化的不可变对象放置在一个有趣的位置,但如果不使用反射,就无法观察到它已经发生了变化

    所以不可变的定义应该是一个不能被观察到已经改变的对象

    如果不可变对象在创建后状态发生变化,但没有人能看到它(没有反射),那么该对象仍然是不可变的吗

  2. # 2 楼答案

    不可变意味着一旦对象的构造函数完成了执行,该实例就不能被更改

    这很有用,因为这意味着您可以传递对对象的引用,而不用担心其他人会更改其内容特别是在处理并发性时,永远不会更改的对象没有锁定问题

    例如

    class Foo
    {
         private final String myvar;
    
         public Foo(final String initialValue)
         {
             this.myvar = initialValue;
         }
    
         public String getValue()
         {
             return this.myvar;
         }
    }
    

    Foo不必担心getValue()的调用方可能会更改字符串中的文本

    如果您设想一个类似于Foo的类,但是成员是StringBuilder而不是String,那么您可以看到getValue()的调用方将能够更改Foo实例的StringBuilder属性

    还要注意你可能发现的不同类型的不变性:Eric Lippert写了一篇关于这一点的文章。基本上,您可以拥有接口不可变但在幕后实际可变的私有状态(因此不能在线程之间安全共享)的对象

  3. # 3 楼答案

    “不可变”意味着您不能更改值。如果您有一个String类的实例,那么您调用的任何似乎修改该值的方法实际上都会创建另一个字符串

    String foo = "Hello";
    foo.substring(3);
    <-- foo here still has the same value "Hello"
    

    要保留更改,您应该执行以下操作 foo=foo。sustring(3)

    使用集合时,不可变与可变可能会很有趣。考虑一下,如果使用可变对象作为map的键,然后更改值,会发生什么情况(提示:考虑equalshashCode

  4. # 4 楼答案

    不可变对象是不能以编程方式更改的对象。它们特别适用于多线程环境或其他多个进程能够改变(变异)对象中的值的环境

    然而,为了澄清,StringBuilder实际上是一个可变的对象,而不是一个不变的对象。常规java字符串是不可变的(这意味着一旦创建了它,就不能在不更改对象的情况下更改基础字符串)

    例如,假设我有一个名为ColoredString的类,它有一个字符串值和一个字符串颜色:

    public class ColoredString {
    
        private String color;
        private String string;
    
        public ColoredString(String color, String string) {
            this.color  = color;
            this.string = string;
        }
    
        public String getColor()  { return this.color;  }
        public String getString() { return this.string; }
    
        public void setColor(String newColor) {
            this.color = newColor;
        }
    
    }
    

    在本例中,ColoredString被称为是可变的,因为您可以更改(变异)它的一个关键属性,而无需创建新的ColoredString类。这可能不好的原因是,例如,假设您有一个GUI应用程序,它有多个线程,并且您正在使用彩色字符串将数据打印到窗口。如果您有一个创建为的ColoredString实例

    new ColoredString("Blue", "This is a blue string!");
    

    然后,您会期望字符串始终为“蓝色”。但是,如果另一个线程获得该实例并调用

    blueString.setColor("Red");
    

    当你想要一根“蓝色”的线时,你会突然,也许是出乎意料地,现在有了一根“红色”的线。因此,在传递对象实例时,几乎总是首选不可变对象。如果确实需要可变对象,则通常只通过从特定的控制域传递副本来保护对象

    用Java来概括一下,Java。String是一个不可变的对象(它一旦创建就不能更改)和java。StringBuilder是一个可变对象,因为它可以在不创建新实例的情况下进行更改

  5. # 5 楼答案

    不可变对象是无法更改内部字段(或至少影响其外部行为的所有内部字段)的对象

    不可变字符串有很多优点:

    性能:执行以下操作:

    String substring = fullstring.substring(x,y);
    

    substring()方法的底层C可能是这样的:

    // Assume string is stored like this:
    struct String { char* characters; unsigned int length; };
    
    // Passing pointers because Java is pass-by-reference
    struct String* substring(struct String* in, unsigned int begin, unsigned int end)
    {
        struct String* out = malloc(sizeof(struct String));
        out->characters = in->characters + begin;
        out->length = end - begin;
        return out;
    }
    

    请注意,无需复制任何字符如果字符串对象是可变的(字符可能稍后更改),则必须复制所有字符,否则子字符串中字符的更改将在稍后反映在另一个字符串中

    并发性:如果不可变对象的内部结构有效,它将始终有效。不同的线程不可能在该对象中创建无效状态。因此,不可变对象是线程安全的

    垃圾收集:垃圾收集器更容易对不可变对象做出逻辑决策

    然而,不变性也有缺点:

    性能:等等,我以为你说性能是不变性的一个优势!嗯,有时是这样,但不总是这样。以下面的代码为例:

    foo = foo.substring(0,4) + "a" + foo.substring(5);  // foo is a String
    bar.replace(4,5,"a"); // bar is a StringBuilder
    

    这两行都将第四个字符替换为字母“a”。第二段代码不仅可读性更强,而且速度更快。看看您必须如何为foo编写底层代码。子字符串很简单,但是现在因为在第五空间已经有一个字符,而其他的东西可能引用了foo,所以你不能仅仅改变它;您必须复制整个字符串(当然,其中一些功能被抽象为实际底层C中的函数,但这里的重点是显示在一个地方执行的代码)

    struct String* concatenate(struct String* first, struct String* second)
    {
        struct String* new = malloc(sizeof(struct String));
        new->length = first->length + second->length;
    
        new->characters = malloc(new->length);
    
        int i;
    
        for(i = 0; i < first->length; i++)
            new->characters[i] = first->characters[i];
    
        for(; i - first->length < second->length; i++)
            new->characters[i] = second->characters[i - first->length];
    
        return new;
    }
    
    // The code that executes
    struct String* astring;
    char a = 'a';
    astring->characters = &a;
    astring->length = 1;
    foo = concatenate(concatenate(slice(foo,0,4),astring),slice(foo,5,foo->length));
    

    注意,concatenate被调用两次,这意味着整个字符串必须循环!将其与bar操作的C代码进行比较:

    bar->characters[4] = 'a';
    

    易变字符串操作显然要快得多

    总之:在大多数情况下,您需要一个不可变的字符串。但是,如果需要在字符串中进行大量的追加和插入,则需要速度的可变性。如果您想获得并发安全和垃圾收集的好处,关键是使可变对象保持在方法的本地:

    // This will have awful performance if you don't use mutable strings
    String join(String[] strings, String separator)
    {
        StringBuilder mutable;
        boolean first = true;
    
        for(int i = 0; i < strings.length; i++)
        {
            if(!first) first = false;
            else mutable.append(separator);
    
            mutable.append(strings[i]);
        }
    
        return mutable.toString();
    }
    

    因为mutable对象是一个本地引用,所以您不必担心并发安全性(只有一个线程接触过它)。由于它不在其他任何地方引用,因此它只在堆栈上分配,因此函数调用完成后它就会被释放(您不必担心垃圾收集)。您可以获得可变性和不可变性的所有性能优势

  6. # 6 楼答案

    1. 在大型应用程序中,字符串文字通常占用大量内存。因此,为了有效地处理内存,JVM分配了一个称为“字符串常量池”的区域。(Note that in memory even an unreferenced String carries around a char[], an int for its length, and another for its hashCode. For a number, by contrast, a maximum of eight immediate bytes is required
    2. 当编译器遇到一个字符串文本时,它会检查池,看看是否已经存在相同的文本。如果找到一个,则对新文本的引用将定向到现有字符串,并且不会创建新的“字符串文本对象”(现有字符串只是获得一个附加引用)
    3. 因此:字符串的易变性节省了内存
    4. 但当任何变量改变值时,实际上——只有它们的引用改变了,而不是内存中的值(因此它不会影响引用它的其他变量),如下所示

    String s1=“旧字符串”

    //s1 variable, refers to string in memory
            reference                 |     MEMORY       |
            variables                 |                  |
    
               [s1]   --------------->|   "Old String"   |
    

    字符串s2=s1

    //s2 refers to same string as s1
                                      |                  |
               [s1]   --------------->|   "Old String"   |
               [s2]   ------------------------^
    

    s1=“新字符串”

    //s1 deletes reference to old string and points to the newly created one
               [s1]   -----|--------->|   "New String"   |
                           |          |                  |
                           |~~~~~~~~~X|   "Old String"   |
               [s2]   ------------------------^
    

    The original string 'in memory' didn't change, but the reference variable was changed so that it refers to the new string. And if we didn't have s2, "Old String" would still be in the memory but we'll not be able to access it...