当前位置: 首页 > article >正文

Java中的String能存储多少字符?不可变吗?

能存储多少字符,通过以下步骤来看

  1. 首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。
  2. 编译器对字符串字面量长度的限制源自Java编译器(如javac)在处理常量池时的实现。编译器源码如下,限制了字符串长度大于等于65535就会编译不通过:
// src/jdk.compiler/share/classes/com/sun/tools/javac/jvm/Pool.java
public class Pool {
    // ...

    /**
     * Add a new Utf8 string to the constant pool, checking for duplicates
     * and sharing the entry if one already exists.
     */
    public int putUtf8(String x) {
        Assert.checkNonNull(x);
        byte[] bytes;
        try {
            ByteArrayOutputStream bytearrayoutputstream = new ByteArrayOutputStream();
            DataOutputStream dataoutputstream = new DataOutputStream(bytearrayoutputstream);
            dataoutputstream.writeUTF(x);
            dataoutputstream.close();
            bytes = bytearrayoutputstream.toByteArray();
        } catch (IOException e) {
            throw new AssertionError(e);
        }
        if (bytes.length > 65535)
            throw new UTFDataFormatException("encoded string too long: " + bytes.length + " bytes");
        return put(new Pool.Utf8Entry(bytes));
    }

    // ...
}

Java中的字符常量都是使用UTF 8编码的,UTF 8编码使用1~4个字节来表示具体的Unicode字符。所以有的字符占用一个字节,而平时所用的大部分中文都需要3个字节来存储。

//65534个字母,编译通过
String s1 = "dd..d";

//21845个中文”自“,编译通过
String s2 = "自自...自";

//一个英文字母d加上21845个中文”自“,编译失败
String s3 = "d自自...自";
  • 对于s1,一个字母d的UTF8编码占用一个字节,65534个字母占用65534个字节,长度是65534,长度和存储都没超过限制,所以可以编译通过。

  • 对于s2,一个中文占用3个字节,21845个正好占用65535个字节,而且字符串长度是21845,长度和存储也都没超过限制,所以可以编译通过。

  • 对于s3,一个英文字母d加上21845个中文”自“占用65536个字节,超过了存储最大限制,编译失败。

当然,这个限制是特定于编译器的实现,而不是Java语言本身的限制。

  1. JVM规范对常量池有所限制。

量池中的每一种数据项都有自己的类型。Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANTUtf8类型表示。CONSTANTUtf8的数据结构如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

重点关注长度为 length 的那个bytes数组,这个数组就是真正存储常量数据的地方,而 length 就是数组可以存储的最大字节数,而不是字符数。length 的类型是u2,u2是无符号的16位整数,因此理论上允许的的最大长度是2^16-1=65535。所以上面byte数组的最大长度可以是65535。

当然,考虑到UTF-8是一种变长编码,一个字符可能需要1到4个字节来表示(取决于字符的具体值)。因此,如果你的字符串包含大量使用多个字节编码的字符,那么它能包含的实际字符数将会少于65535。

  1. 运行时限制

String 运行时的限制主要体现在 String 的构造函数上。下面是 String 的一个构造函数:

public String(char value[], int offset, int count) {
    ...
}

上面的count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1。所以在运行时,String 的最大长度是2^31-1。

但是这个也是理论上的长度,实际的长度还要看JVM的内存。来看下,最大的字符串会占用多大的内存。

highlighter-

(2^31-1)*16/8/1024/1024/1024 = 2GB

所以在最坏的情况下,一个最大的字符串要占用4GB的内存。如果JVM不能分配这么多内存的话,会直接报错的。

总结

因此,主要的还是看编译器对常量池的限制,使得byte数组的最大长度不能超过65535;以及JVM的内存限制

补充:JDK9以后对String的存储进行了优化。底层不再使用char数组存储字符串,而是使用byte数组。对于LATIN1字符的字符串可以节省一倍的内存空间。

Java中的String是不可变对象

在面向对象及函数编程语言中,不可变对象(英语:Immutable object)是一种对象,在被创造之后,它的状态就不可以被改变。至于状态可以被改变的对象,则被称为可变对象(英语:mutable object)。-- 来自百度百科

Java8 String源码

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];//Java 9已经优化为byte数组了

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    
    ...
}

显然String字符串内部是使用char[]数组来存储。

而这个char[]数组是用 private final来修饰的,private就体现着面向对象的封装特性,并且String没有提供供外部访问的方法,这就意味着这个属性无法被外部访问;final则意味着这个属性无法修改,无法重新指向其他对象。且String 类没有提供/暴露修改这个字符串的方法。

因此,String是不可变对象

不可变的优点

  • 线程安全。同一个字符串实例可以被多个线程共享,因为字符串不可变,本身就是线程安全的。
  • 支持hash映射。因为String的hash值经常会使用到,比如作为 Map 的键,不可变的特性也就使得hash值不会变,不需要重新计算。
  • 字符串常量池优化。String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。

一定不可变吗

事实上,可以通过反射来改变String中的值

String str = "abcdef";
System.out.println("修改前的地址值:" + str + ",hash值"+ str.hashCode());
Class<? extends String> aClass = str.getClass();
Field value = aClass.getDeclaredField("value");
value.setAccessible(true);
value.set(str,"seven".getBytes());
System.out.println("修改后的地址值:" + str + ",hash值"+ str.hashCode());

显然修改后的还是同一个地址和hash值

修改前的地址值:abcdef,hash值-1424385949
修改后的地址值:seven,hash值-1424385949

查看源码可以看到,计算hashcode后hash值是由一个常量缓存下来的,所以通过反射修改后hashCode并不会变,除非进行重新计算。

注意:用反射修改String的值破坏了String的immutable特征,可能会带来各种问题,以上只是提供一个思路,不建议这么做。

不可变类都建议参考String类一样,写个变量缓存hashcode,从而防止高并发下的计算

public int hashCode() {
    int h = hash;
    if (h == 0 && !hashIsZero) {
        h = isLatin1() ? StringLatin1.hashCode(value)
                       : StringUTF16.hashCode(value);
        if (h == 0) {
            hashIsZero = true;
        } else {
            hash = h;
        }
    }
    return h;
}


http://www.kler.cn/a/286714.html

相关文章:

  • Unity自学之旅05
  • 【React】 react路由
  • FPGA自分频产生的时钟如何使用?
  • 数据缺失补全方法综述
  • AIGC专栏18——EasyAnimateV5.1版本详解 应用Qwen2 VL作为文本编码器,支持轨迹控制与相机镜头控制
  • centos9编译安装opensips 二【进阶篇-定制目录+模块】推荐
  • 这才是老板喜欢的运营简历
  • three.js 编辑器,动画,着色器, cesium 热力图,聚合点位,大量点线面, 图层,主题,文字,等众多案例中心
  • OmniGraffle Pro for Mac 思维导图软件安装
  • 前端打包部署,Nginx服务器启动
  • 【protobuf】protobuf语法及序列化原理
  • 集成电路学习:什么是DMA直接内存访问
  • 虚幻5|技能栏UI优化(2)——优化技能UI并实现技能栏的拖拽操作
  • 日常工作中的AI助手
  • milvus多个Querynode,资源消耗都打在一个节点上
  • C语言6大常用标准库 -- 1.<stdio.h>
  • Day-01—QT项目的新建
  • 【大模型LLM第十一篇】微调自动化数据选择方式之MoDS
  • 不良信息公示
  • How can I change from OpenAI to ChatOpenAI in langchain and Flask?
  • 介绍一下最近很火的一款游戏黑神话悟空,以及国产游戏面临的挑战
  • AWS api数据信息获取(boto3)
  • 【数据结构】优先级队列 — 堆
  • kafka使用
  • 基于asp.net软件缺陷跟踪系统设计与实现
  • Java:数字验证