Java基础(字符串篇)

说明

本文内容基于JDK8,其他版本JDK可能会有所差异。

参考来源:

  • String API
  • StringBuffer api
  • StringBuilder api
  • 概念

    官方定义
    The String class represents character strings. All string literals in Java programs, such as “abc”, are implemented as instances of this class.Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.
    百度翻译
    String类表示字符串。Java程序中的所有字符串文本(如“abc”)都作为此类的实例实现。字符串是常量;它们的值在创建后不能更改。字符串缓冲区支持可变字符串。因为字符串对象是不可变的,所以它们可以共享。

    特点

    String类3个特性: 不可改变(immutable)、常量池优化、final定义不可继承;
    String类有三个重要的部分,char数组、length长度,还有一个offset偏移量;其中char数组表示字符串内容;偏移量和长度用来定位和截取;

    String的长度限制
    因为char数组的长度是int的,而int是4个字节表示,所以最大长度为Integer.MAX_VALUE = 0x7fffffff=2^31 - 1 = 2147483647 = 2 * 2^10 * 2^10 * 2^10 -1 = 2G-1 长度; 所以String最多能存储2GB-1大小的字符串;但是实际上在win10和ubuntu 64位操作系统中实际上数组的实际的有效长度为Integer.MAX_VALUE-2 (容量的话需要根据字符串内容的编码,中英文等来决定,比如UTF-8的中文占用3字节=3Byte=24bit=24位,GBK的中文占用2字节;英文占用1字节等),所以实际上字符串的长度只能到2G-3的长度(中文英文都可以);
    注意: 但是如果含有中文,且很长,那么在getBytes的时候可能会因为中文占用更多字节导致byte数组长度不够导致报错OutOfMemoryError: Requested array size exceeds VM limit;

    编译时常量字符串长度
    但是如果是在代码中就编码好的字符串常量,则无法达到以上长度,因为编译时就会报错,因为jvm编译规范规定了字符串常量的长度为2个字节表示,所以最大长度为2^16- 1 = 65535 = 64 * 2^10 - 1 = 64KB - 1;但是因为jvm还需要1个字节的指令作为结束,所以实际的有效长度为 65534 = 64K -2

        /**
         * 字符串最大长度 2G - 3 长度个字符(中文英文都可以)
         */
        public static void lengthTest() {
            int max = Integer.MAX_VALUE - 2;//2147483645
            StringBuilder sb = new StringBuilder(max);//在win10和ubuntu 64位环境下,数组大小只能到Integer.MAX_VALUE-2;否则会报错OutOfMemoryError: Requested array size exceeds VM limit
            for (int i=0; i < max; i ++) {
                sb.append("苏");
            }
            System.out.println(sb.length());
    //        System.out.println(sb.toString());//如果需要打印字符串,1是要很久,2是堆内存需要设很大,否则会OOM,例如可以直接设个-Xmx31g
            writeToFile(sb.toString(), "/sz");
        }
    
        /**
         * 常量字符串长度 65534 = 64K - 2 长度个字符(中文英文都可以)
         */
        public static void lengthTest1() {
            int max = 1 << 16;//2^16 = 65536
            StringBuilder sb = new StringBuilder(max);
            for (int i=0; i < max-1; i ++) {//生成65535长度字符串
                sb.append("苏");
            }
            char c = '苏';//一个char字符是可以存储一个中文的
            System.out.println(sb.length());//65535
            String str = sb.toString();
            System.out.println(str);
            writeToFile(str, "D:/sz");
            String s = "";//把打印的65535长度字符串放进来进行编译,编译报错Error: java: 常量字符串过长;把65535长度字符串删掉一个字母变成65534长度字符串,编译通过;
        }
    
        /**
         * 将字符串写入文件
         * @param content
         * @param fileName
         */
        public static void writeToFile(String content, String fileName) {
            try {
    //            RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
    //            raf.write(content.getBytes("utf-8"));//因为中文占用更多字节,如果长度太长会报错OutOfMemoryError: Requested array size exceeds VM limit
                FileWriter fw = new FileWriter(fileName);
                fw.write(content);//通过String的方式(底层是char数组的方式)写入Integer.MAX_VALUE - 2长度的中文没有问题,不会出现数组长度问题;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    不变性

    String对象一旦创建生成,就不能再对其进行改变;它的不变模式在多线程下可以省略同步和锁等操作,从而可以提高性能和简化复杂度;其他的不可变类型还有8个基本类型的包装类、BigInteger、BigDecimal等;
    String的不变特性是通过private+final的方式实现的;但是不意味着它一定无法更改,通过反射机制来实现;
    因为String的不可变性,有些情况下是会额外消耗资源的;所以在fastjson2中使用了LambdaMetafactory 来创建函数映射调用,避免字符串的复制,从而实现零拷贝的方法;详情见:fastjson2为什么这么快?

        /**
         * 通过反射手段来破坏String的不可变性
         */
        public static void changeString() {
            try {
                String str = "sz";
                System.out.println(str);//ss
                Field field = String.class.getDeclaredField("value");
                field.setAccessible(true);
                char[] value = (char[]) field.get(str);
                value[1] = 's';
                System.out.println(str);//sz
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    常量池

    new关键字等产生的String对象再堆中,而在堆中进行的对象生成的过程是不会去检测该对象是否已经存在的;而常量字符串则会放置到常量池中;类变量的常量赋值会在类初始化加载的时候就进入常量池,方法内部的常量赋值,需要在执行时才会入常量池(如果存在,则使用);堆中的字符串对象可以通过需要调用intern()方法来放入常量池;

    //    private static String s = "sz"; 如果该语句不注释,下方的testIntern方法的测试结果打印全部为false
        /**
         *  A pool of strings, initially empty, is maintained privately by the class String.
         *  When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
         *  It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
         *  调用intern方法,会将产生的字符串对象置入常量池(若不存在则置入),若常量池中已经存在该对象,则返回常量池中的字符串对象引用。
         */
        public static void testIntern() {
            String str2 = new StringBuilder("ja").append("va").toString();
            System.out.println(str2.intern() == str2);//false 说明常量池已经存在java字符串,应该是java虚拟机初始化加载一些基础信息的时候就把java这个特殊字符加入了常量池
    
            String str3 = new StringBuilder("s").append("z").toString();
            String str5 = new StringBuilder("s").append("z").toString();
            System.out.println(str3.intern() == str3);//true 第一次进入常量池
    
            String str4 = "sz";
            System.out.println(str4 == str3);//true
            System.out.println(str5 == str3);//false
    
            System.out.println(str5.intern() == str5);//false 说明常量池已经存在
            System.out.println(str5.intern() == str3);//true
        }
    
        /**
         *  当两个字符串的值相同时,它们其实都是使用的常量池中的同一个拷贝,这样做可以节省内存空间;
         */
        public static void testIntern2() {
            String str1 = "aaa";
            String str2 = "aaa";
            String str3 = new String("aaa");
            char data[] = {'a', 'a', 'a'};
            String str4 = new String(data);
            System.out.println(str1 == str2);//true
            System.out.println(str1 == str3);//false str3对象新开辟了内存空间
            System.out.println(str1 == str4);//false str4对象新开辟了内存空间
            System.out.println(str1 == str3.intern());//true str3对象所对应的常量池中的位置是同一个
            System.out.println(str1 == str4.intern());//true str4对象所对应的常量池中的位置是同一个
        }
    

    final定义

    final修饰类,从而无法被继承,方法不能被重写,变量不能被修改;

    注意事项

    字符编码

    ASCII

    ASCII(American Standard Code for Information Interchange),美国信息互换标准代码;使用7位或8位二进制数组合来表示128 或256 种可能的字符。
    标准ASCII 码也叫基础ASCII码,使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符 。

    0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符)
    如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;
    通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;
    ASCII值为8、9、10 和13 分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响 。

    32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字。

    65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。

    Unicode

    Unicode(统一码、万国码、单一码)是国际组织制定的旨在容纳全球所有字符的编码方案,是计算机科学领域里的一项业界标准,包括字符集、编码方案等。
    UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码实现,UTF-8用1到4个字节编码Unicode字符。一般英文字占用1个字节;中文占用3个字节。
    UTF-8 规则:
    a. 对于1字节的字符,字节的第1位为0,后7位为这个符号的unicode码。因此英语字母在UTF-8编码和ASCII码是相同的。
    b. 对于大于1字节(n字节)的字符,第1个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
    Java日常开发中一般推荐使用UTF-8。
    其他更多更详细内容自行搜索。

    字符串拼接

    String进行常量模式的创建和累加拼接;如果时显示的常量累加模式,则在编译阶段就会优化成一个字符串,这种时没有必要使用StringBuilder来构建的;但是如果时变量相加的模式,则有些情况下编译时会进行优化,有时候是不会进行优化的;
    结论:对于显示的常量字符串拼接,可以使用+运行拼接;如果涉及到变量拼接的方式,则使用StringBuilder或者StringBuffer(线程安全);如果能预计出容量大小(不指定初始大小是16),可以设置大小来减少内存占用和优化速度;

    /**
         * 字符串拼接
         */
        public static void plusTest() {
            String a = "abc";
            String d = "def";
    
            String s1 = "abc" + "def";//这种常量累加的方式在编译阶段就会优化变成一个字符串; String s1 = "abcdef";
            String s2 = a + d;//这个后面有使用到,所以编译时没有优化
            StringBuilder sb = new StringBuilder();
            sb.append("abc");
            sb.append("def");
            String s3 = sb.toString();
            System.out.println(s1 == s2);//false
            System.out.println(s1 == s3);//false
            System.out.println(s1 == s2.intern());//true 对象所对应的常量池中同一个
            System.out.println(s1 == s3.intern());//true 对象所对应的常量池中同一个
            String s = a + d;//这种模式如果字符串后面没有被使用,会优化成 (new StringBuilder()).append(a).append(d).toString();
            //System.out.println(s);//如果有这个逻辑存在,则编译的时候不会对String s = a + d进行优化;
        }
    
        /**
         * 字符串拼接性能对比测试
         */
        public static void plusPerformanceTest() {
            int count = 10000;
            long t = System.currentTimeMillis();
    //        String str = "";
    //        for (int i=0; i < count; i ++) {
    //            str += i;//348ms
    //        }
            StringBuilder sb = new StringBuilder();
            for (int i=0; i < count; i ++) {
                sb.append(i);//1ms
            }
            sb.toString();
    //        String s = "";
    //        for (int i=0; i < count; i ++) {
    //            s = s.concat(String.valueOf(i));//73 ms
    //        }
            long t1 = System.currentTimeMillis();
            System.out.println(t1-t);
        }
    
    /**
         * 字符串拼接性能对比测试
         */
        public static void plusPerformanceTest1() {
            int count = 5000 * 10000;
            long t = System.currentTimeMillis();
    //        StringBuilder sb = new StringBuilder();// 602ms sb.capacity()=75497470 sb.length()=50000000
    //        StringBuilder sb = new StringBuilder(count);// 424ms sb.capacity()=50000000 sb.length()=50000000
    //        StringBuffer sb = new StringBuffer();//1862 ms sb.capacity()=75497470 sb.length()=50000000
            StringBuffer sb = new StringBuffer(count);//1643 ms sb.capacity()=50000000 sb.length()=50000000
            for (int i=0; i < count; i ++) {
                sb.append("a");
            }
            sb.toString();
            long t1 = System.currentTimeMillis();
            System.out.println(t1-t);
            System.out.println(sb.length());
            System.out.println(sb.capacity());
        }
    

    StringBuilder和StringBuffer都继承了AbstractStringBuilder,在append操作时,都会进行容量判断,如果容量不够,则进行扩容成2倍+2,如果还不够,则构建对应的大小;然后进行数组复制(Arrays.copyOf >> System.arraycopy System.arraycopy)

        public AbstractStringBuilder append(String str) {
            if (str == null)
                return appendNull();
            int len = str.length();
            ensureCapacityInternal(count + len);
            str.getChars(0, len, value, count);
            count += len;
            return this;
        }
        private void ensureCapacityInternal(int minimumCapacity) {
            // overflow-conscious code
            if (minimumCapacity - value.length > 0) {
                value = Arrays.copyOf(value,
                        newCapacity(minimumCapacity));
            }
        }
        private int newCapacity(int minCapacity) {
            // overflow-conscious code
            int newCapacity = (value.length << 1) + 2;//扩容成 *2 + 2 这里为什么+2,目前不清楚目的
            if (newCapacity - minCapacity < 0) {//如果还不够,则构建对应的大小;
                newCapacity = minCapacity;
            }
            return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
                ? hugeCapacity(minCapacity)
                : newCapacity;
        }
    

    字符串数组复制

    System.arraycopy复制String数组;虽然System.arraycopy是浅拷贝(shallow copy),只复制数组中的元素即对象引用。此时如果数组是String与8种包装类型、BigInteger、BigDecimal等不可变<不可变的意思是每次更换值都会重新生成对象并赋给引用>,则可以算是值传递,因为你没法修改原来的对象(前提是不使用反射等手段来破坏它的不可变性);如果是其他引用类型,则修改对象时,那么对象的改变会反应到所有的引用的地方。

        /**
         * 不可变类型String复制
         */
        public static void copyStringArray() {
            String[] a1 = {"1","2"};
            String[] a2 = {"11", "AA"};
            String[] a3 = new String[4];
            System.arraycopy(a1, 0 , a3, 0, 2);
            System.arraycopy(a2, 0 , a3, 2, 2);
            System.out.println(Arrays.toString(a3));//[1, 2, 11, AA]
            String s = a2[1].toLowerCase();//没法修改原来的对象,操作后会变成一个新的对象,原对象不变;
            System.out.println(s);//aa
            System.out.println(Arrays.toString(a2));//[11, AA]
            System.out.println(Arrays.toString(a3));//[1, 2, 11, AA]
            System.out.println(a1[1] == a3[1]);//true
        }
         /**
         * 不可变类型String复制,通过反射来破坏不可变
         */
        public static void copyStringArray1() {
            String[] a1 = {"1","2"};
            String[] a2 = {"11", "AA"};
            String[] a3 = new String[4];
            System.arraycopy(a1, 0 , a3, 0, 2);
            System.arraycopy(a2, 0 , a3, 2, 2);
    
            System.out.println(Arrays.toString(a3));//[1, 2, 11, AA]
            try {
                Field field = String.class.getDeclaredField("value");
                field.setAccessible(true);
                char[] value = (char[]) field.get(a2[1]);
                value[1] = 's';
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Arrays.toString(a2));//[11, As]
            System.out.println(Arrays.toString(a3));//[1, 2, 11, As]
            System.out.println(a1[1] == a3[1]);//true
        }
    

    字符串截取

    比较常用的截取字符串方法是String提供的substring(int beginIndex, int endIndex)。
    这里单独说一下:
    a. 因为有看到过有书上说这个方法调用的String的一个构造函数String(char value[], int offset, int count)采用了空间换时间的策略,直接将原字符串返回了(this.value=value),只是记录了offset和count而并没有真正的截取;这种方式在大量长字符串的截取中可能会造成极大的空间浪费从而有OOM风险;而且有好几个字符串操作都使用到了那个构造方法;
    b. 我从JDK8的源码看到,实际构造函数String(char value[], int offset, int count)已经是通过数组复制的方式实现;所以就没有书中说的那种情况了;

        public String(char value[], int offset, int count) {
            if (offset < 0) {
                throw new StringIndexOutOfBoundsException(offset);
            }
            if (count <= 0) {
                if (count < 0) {
                    throw new StringIndexOutOfBoundsException(count);
                }
                if (offset <= value.length) {
                    this.value = "".value;
                    return;
                }
            }
            // Note: offset or count might be near -1>>>1.
            if (offset > value.length - count) {
                throw new StringIndexOutOfBoundsException(offset + count);
            }
            this.value = Arrays.copyOfRange(value, offset, offset+count);
        }
    
    /**
         * 根据index截取字符串
         */
        public static void subStringTest() {
            List<String> list = new ArrayList<>();
    
            for (int i=0; i < 100000; i++) {
                String str = new String(new char[100000]);
                list.add(str.substring(1, 3));
            }
        }
    

    字符串替换

    字符串替换,注意因为字符串的不可变性,如果对长字符串进行大量替换操作会产生大量的中间临时字符串;replace就是把匹配到的字符串全部替换掉;replaceAll则是支持正则表达式,其他能力和replace一样;replaceFirst也支持正则表达式,只是只替换第一个匹配到的字符串;

        /**
         * 字符串替换,注意因为字符串的不可变性,如果对长字符串进行大量替换操作会产生大量的中间临时字符串;
         */
        public static void replaceStringTest() {
            String s = "asdfghjklas";
            String r = s.replace("a", "z");
            System.out.println(r);//zsdfghjklzs
            r = s.replaceAll("[a|d]", "z");//szsdfghjklszs
            System.out.println(r);//zszfghjklzs
            r = s.replaceFirst("[a|d]", "z");//szsdfghjklszs
            System.out.println(r);//zsdfghjklas
    
        }
    
    

    字符串分割

    如果想将字符串按照某个字符串(也可以是正则表达式)分割成一个字符串数组,则可以使用split;
    如果是想按照某个字符或者串分割,且进行遍历,则可以考虑使用StringTokenizer处理;

        /**
         * 字符串分割
         */
        public static void splitTest() {
            String s = "a;b,c d";
            String[] r = s.split("[;|,| ]");
            System.out.println(Arrays.toString(r));//[a, b, c, d]
        }
        
        /**
         * 两种分隔字符串的比较,从我的机器上跑了多次来看,性能差不多,只是一个是数组方式,一个是遍历方式;
         * 这个和有些书上讲的split性能比StringTokenizer差的结果不符;应该是jdk8进行了优化或者是书本错误;
         */
        public static void splitPerformanceTest() {
            StringBuilder sb = new StringBuilder();
            for (int i=0; i < 1000; i ++) {
                sb.append(i).append(";");
            }
            String str = sb.toString();
            long t0 = System.currentTimeMillis();
            for (int i=0; i < 50000; i ++) {
                String[] r = str.split(";");
    
    //            StringTokenizer st = new StringTokenizer(str, ";");
    //            while (st.hasMoreTokens()) {
    //                String s = st.nextToken();
    //            }
    
            }
            long t1 = System.currentTimeMillis();
            System.out.println(t1-t0);
    
        }
    

    字符串查找

    几种常用的查找处理字符串的方法;

        /**
         * 字符串查找相关方法
         */
        public static void findTest() {
            StringBuilder sb = new StringBuilder();
            sb.append("java");
            for (int i=0; i < 100; i ++) {
                sb.append(i).append(";");
            }
            sb.append("java");
            String str = sb.toString();
    
            int index = str.indexOf(";");//底层实现是先尝试找到匹配的第一个字符,然后基于这开始依此比较剩下的字符,直到找到为止;
            char c = str.charAt(1);
    
            str.endsWith("java");//底层是一个字符一个字符比较的,所以下面的那种实现方式没有必要(因为有些书上推荐,应该是jdk后面优化过或者书本错误)
    //        int len = str.length();
    //        if (str.charAt(len-4) == 'j'
    //                && str.charAt(len-3) == 'a'
    //                && str.charAt(len-2) == 'v'
    //                && str.charAt(len-1) == 'a') {
    //
    //        }
    
            str.startsWith("java");//底层是一个字符一个字符比较的,所以下面的那种实现方式没有必要(因为有些书上推荐,应该是jdk后面优化过或者书本错误)
    //        if (str.charAt(0) == 'j'
    //                && str.charAt(1) == 'a'
    //                && str.charAt(2) == 'v'
    //                && str.charAt(3) == 'a') {
    //
    //        }
    
        }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值