Java CharAt()和deleteCharAt()性能

Java CharAt() and deleteCharAt() performance

我一直想知道java中String / StringBuilder / StringBuffer的charAt函数的实现
它的复杂性是什么?
还有StringBuffer / StringBuilder中的deleteCharAt()呢?


对于StringStringBufferStringBuildercharAt()是恒定时间操作。

对于StringBufferStringBuilderdeleteCharAt()是线性时间操作。

StringBufferStringBuilder具有非常相似的性能特征。主要区别在于前者是synchronized(因此是线程安全的),而后者不是。


让我们依次查看这些方法中的每一个的相应的实际Java实现(仅相关代码)。这本身将回答他们的效率。

String.charAt:

1
2
3
4
5
6
public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

如我们所见,这只是一个单一数组访问,这是一个恒定时间的操作。

StringBuffer.charAt:

1
2
3
4
5
public synchronized char charAt(int index) {
  if ((index < 0) || (index >= count))
    throw new StringIndexOutOfBoundsException(index);
  return value[index];
}

同样,单阵列访问,因此是恒定时间的操作。

StringBuilder.charAt:

1
2
3
4
5
public char charAt(int index) {
    if ((index < 0) || (index >= count))
        throw new StringIndexOutOfBoundsException(index);
    return value[index];
}

同样,单阵列访问,因此是恒定时间的操作。即使这三种方法看起来都一样,也存在一些细微的差异。例如,仅StringBuffer.charAt方法被同步,而其他方法则不同步。同样,如果检查String.charAt是否略有不同(猜为什么)。仔细研究这些方法实现本身会给我们带来其他小的差异。

现在,让我们看一下deleteCharAt的实现。

字符串没有deleteCharAt方法。原因可能是它是一个不变的对象。因此,公开一个明确指示该方法修改对象的API可能不是一个好主意。

StringBuffer和StringBuilder都是AbstractStringBuilder的子类。这两个类的deleteCharAt方法将实现委派给其父类本身。

StringBuffer.deleteCharAt:

1
2
3
4
  public synchronized StringBuffer deleteCharAt(int index) {
        super.deleteCharAt(index);
        return this;
    }

StringBuilder.deleteCharAt:

1
2
3
4
 public StringBuilder deleteCharAt(int index) {
        super.deleteCharAt(index);
        return this;
    }

AbstractStringBuilder.deleteCharAt:

1
2
3
4
5
6
7
  public AbstractStringBuilder deleteCharAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        System.arraycopy(value, index+1, value, index, count-index-1);
        count--;
        return this;
    }

仔细研究AbstractStringBuilder.deleteCharAt方法可以发现它实际上是在调用System.arraycopy。在最坏的情况下,该值为O(N)。因此deleteChatAt方法的时间复杂度为O(N)。


charAt方法是O(1)

假设您要从N字符StringBuffer / StringBuilder中删除随机字符,则StringBuilderStringBuffer上的deleteCharAt方法平均为O(N)。 (它必须平均移动剩余字符的一半来填充已删除字符留下的"空洞"。在多个操作中没有摊销;请参见下文。)但是,如果删除最后一个字符,则成本将为O(1)

String没有任何deleteCharAt方法。

从理论上讲,StringBuilderStringBuffer可以针对在"通过"缓冲区中插入或删除多个字符的情况进行优化。他们可以通过在缓冲区中保留一个可选的"间隙"并在其中移动字符来实现此目的。 (IIRC,emacs以这种方式实现其文本缓冲区。)这种方法的问题是:

  • 它需要更多的空间,用于表示间隙在哪里的属性以及间隙本身。
  • 它使代码复杂得多,并且减慢了其他操作的速度。例如,charAt必须将offset与间隙的起点和终点进行比较,并在获取字符数组元素之前对实际的索引值进行相应的调整。
  • 如果应用程序在同一缓冲区上执行多次插入/删除操作,这只会有所帮助。

毫不奇怪,这种"优化"尚未在标准StringBuilder / StringBuffer类中实现。但是,自定义CharSequence类可以使用此方法。


charAt超级快(并且可以将内部函数用于String),它是数组的简单索引。 deleteCharAt将需要arraycopy,因此删除字符不会很快。


众所周知,字符串在JDK中作为字符数组实现,该数组实现了randomAccess接口。因此,charAt的时间复杂度应为int O(1)。与其他阵列一样,删除操作的时间复杂度为O(N)