【问题标题】:Is directly accessing the backing array of a String justified in some cases?在某些情况下,直接访问 String 的后备数组是否合理?
【发布时间】:2012-12-18 13:52:50
【问题描述】:

我正在优化文本处理软件,其中使用了以下类:

class Sentence {

  private final char[] textArray;
  private final String textString; 

  public Sentence(String text) {
     this.textArray = text.toCharArray();
     this.textString = text;
  }

  public String getString() {
     return textString;
  }

  public char[] getArray() {
     return textArray;
  } 
}

如您所见,存在一些冗余:textString 的支持数组始终等于 textArray,但两者都被存储。

我希望通过去掉 textArray 字段来减少此类的内存​​占用。

有一个问题:这个类在代码库中被广泛使用,因此我无法摆脱 getArray() 方法。我的解决方案是去掉 textArray 字段,让 getArray() 方法通过反射返回 textSting 的支持数组。

结果会是这样的:

class Sentence {

  private final String textString; 

  public Sentence(String text) {
       this.textString = text;
  }

  public String getString() {
     return textString;
  }

  public char[] getArray() {
     return getBackingArrayUsingReflection(textString);
  } 
}

这似乎是一个可行的解决方案,但我怀疑 String 的后备数组是私有的是有原因的。这种方法有哪些潜在问题?

【问题讨论】:

  • 为什么不返回 textString.toCharArray()?
  • 因为textString.toCharArray()每次调用都会创建一个新数组,而且调用的频率很高。
  • 也许您可以重构代码以使用您自己的CharSequence 实现,而不是在String 上使用反射。

标签: java arrays string reflection


【解决方案1】:

将会发生的一件事是您将自己提交给 JDK 的一个特定实现。例如,Java 7 Update 6 完全修改了其对char[] 的使用。这就是为什么只有当你的代码是非常短暂的,基本上是一次性的代码时才应该容忍这种方法。

如果您只阅读char[],并且您正在为 OpenJDK Java 7 Update 6 编写代码,则不会引入任何错误。

另一方面,全世界 95% 的 Java 程序员可能会对反映 String 内部结构的代码表示怀疑,所以要小心 :)

【讨论】:

  • 其实你会引入bug,看我的回答。
  • +1 Java 6 的某些版本将使用 byte[] 以节省内存,例如-XX:+UseCompressedStrings
  • @MarkRotteveel 正如我所指出的,他不会将错误引入一个特定的 JDK 实现,即 Java 7 Update 6。
  • 哇,投了反对票。我什至不会要求解释,因为不可能有任何解释。 Downvoter,今天过得不好?
【解决方案2】:

根据java.lang.String(Java 7 Update 5 及更早版本)的版本,它使用一个后备数组,以及该数组中实际字符串的开始索引和长度 (count)。在 Java 的这些实现中,后备数组可以(基本上)比实际字符串长,并且字符串不一定从数组的开头开始。

例如,当您使用substring 时,后备数组可能与原始字符串的后备数组相同,只是起始索引和字符数不同。因此,使用反射返回 String 的支持数组并非在所有情况下都有效(或者:它会导致不正确/意外的行为)。

参见例如http://www.docjar.com/html/api/java/lang/String.java.htmlString substring(int beginIndex, int endIndex) 在第 1950 行(及以下),它调用构造函数 String(int offset, int count, char value[]) 在第 645 行(及以下)。这里char[]直接作为后备数组,offset和count分别作为到数组的偏移量和字符串的长度:

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
}

// Package private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

正如 Marko Topolnik 所指出的,更多的recent versions of Java 7 不再是这种情况。您不应该依赖于 Java 的实现细节(尤其是因为它可以在版本之间发生重大变化 - 如图所示)。

【讨论】:

  • 最新的 Java 7 不正确。请参阅here 以获得证明。
  • @MarkoTopolnik 供参考,this is what really happened
  • @assylias 伟大的挖掘!将其添加为书签以供将来参考。
【解决方案3】:

如果您想要更快,请使用String.charAt(i),它将被内联并避免任何与更改 inetrnals 相关的问题。如果你想避免从 StringBuilder 创建 String,你可以使用 CharSequence,因为它们都支持这个接口。

【讨论】:

  • 我也更喜欢 charAt(i),但是有很多遗留代码使用字符数组,所以我的 Sentence 类必须以某种方式公开字符数组。
  • 如果你最常访问一个char[],你可以存储char[] textArray,在需要的时候实例化一个新的String。
  • 好点,但我访问字符串和 char[] 的频率大致相等
【解决方案4】:

为了娱乐和游戏,运行以下单元测试:

public class StringTest {
    private String text;

    public StringTest() {
        super();
    }

    public char[] getBackingArray() {
        if (text == null) {
            return null;
        }

        try {
            final Field valueField = text.getClass().getDeclaredField("value");
            valueField.setAccessible(true);
            final char[] data = (char[]) valueField.get(text);
            return data;
        } catch (final Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    @Test
    public void testStringFunManipulation() {
        final StringTest test = new StringTest();
        test.setText("Hello World");
        Assert.assertNotNull(test);
        System.out.println("Original String: " + test);
        System.out
                .println("Original String Hash: " + test.getText().hashCode());

        char[] data = test.getBackingArray();
        Assert.assertNotNull(data);
        System.out.println("Backing Array: " + data);

        data[0] = 'J';
        System.out.println("Modified String: " + test);
        System.out
                .println("Modified String Hash: " + test.getText().hashCode());
        System.out.println("Modified String Hash Should be: "
                + "Jello World".hashCode());
    }

    @Override
    public String toString() {
        return text != null ? text.toString() : "";
    }
}

它应该可以回答为什么公开类的内部私有值可能是个坏主意。

【讨论】:

  • 是的,直接更改字符串的后备数组会导致 hashCode 与值不同步。幸运的是,我只需要从数组中读取,从不写入。
  • @SynthC - 一旦暴露,你真的会失去对如何使用这个数组的控制。您的应用程序的内存受限程度如何?
  • 内存实际上并不是最大的问题:应用程序是分布式的,因此网络吞吐量是一个更大的问题。我希望通过公开支持数组来实现更小的序列化句子。这似乎是一个简单的解决方案,但可能不值得潜在的错误,我将专注于优化序列化过程。
【解决方案5】:

您可以按如下方式更改getArray 实现:

public char[] getArray() 
{
    return this.textString.toCharArray();
} 

【讨论】:

  • 我怀疑如果多次调用它会更慢。
  • 是的,同意。但对我来说看起来更具可读性和易于理解。
  • 你是否意识到他的第一个版本的代码就是这样做的,但更好?
猜你喜欢
  • 2018-05-22
  • 2011-04-30
  • 2018-05-04
  • 2011-04-17
  • 2015-09-15
  • 2020-09-02
  • 1970-01-01
  • 1970-01-01
  • 2011-11-22
相关资源
最近更新 更多