Java 总结第一弹

Java 字符串

字符串 (续)

1.6 String 的 intern() 方法

intern() 这个方法是一个 native 的方法

(使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用)

  • 如果常量池中存在当前字符串, 就会直接返回当前字符串, 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回

String.intern() 方法在执行时的策略

  • Java 7 之前,执行 String.intern() 方法的时候,不管对象在堆中是否已经创建,字符串常量池中仍然会创建一个内容完全相同的新对象,将创建的对象的引用放回

  • Java 7 之后,由于字符串常量池放在了堆中,执行 String.intern() 方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象,而是直接保存堆中对象的引用,然后将其返回,同时节省了一部分的内存空间

  • 经典例题

    public static void main(String[] args) {
        String s = new String("1");
        /**
         * 堆中存在一个对象:
         * String("1")
         * 字符串常量池中存在一个对象:
         * String("1")
         * 将堆中的String("1")的引用返回
         */
        s.intern();
        // 字符串常量池中存在,不作操作
        String s2 = "1";
        /**
         * 显示声明,先看字符串常量池中是否拥有该对象(或者该对象的引用)
         * 存在:直接将其引用返回
         * 不存在:直接在字符串常量池中创建并返回该对象的引用
         * 此时存在String("1"),直接将其引用返回
         */
        System.out.println(s == s2);
        // 一个是堆中对象的引用,一个是字符串常量池中对象的引用
    
        String s3 = new String("1") + new String("1");
        /**
         * 堆中存在三个对象:
         * 一个StringBuilder("11")对象,两个String("1","11")对象
         * 字符串常量池中存在一个对象:
         * String("1")
         * 将堆中的String("11")的引用返回
         */
        s3.intern();
        /**
         * 此时,字符串常量池中没有Stirng("11")的对象
         * JDK 7之前:直接在字符串常量池中创建一个Stirng("11")的对象
         * JDK 7之后:将堆中的Stirng("11")对象的引用保存一份到字符串常量池中
         */
        String s4 = "11";
        /**
         * 显示声明,先看字符串常量池中是否拥有该对象(或者该对象的引用)
         * 存在:直接将其引用返回
         * 不存在:直接在字符串常量池中创建并返回该对象的引用
         * 由于调用了intern()方法:
         * 	JDK 7之前:此时字符串常量池中存在Stirng("11")的对象
         * 	JDK 7之后:此时字符串常量池中存在堆中Stirng("11")的对象的引用
         * 将调用了intern()方法的结果返回
         */
        System.out.println(s3 == s4);
        /**
         * 前提:先调用intern()方法后再进行双引号创建
         * JDK 7之前:一个是堆中对象的引用,一个是字符串常量池中对象的引用
         * JDK 7之后:两个都是堆中对象的引用
         */
    }
    // JDK 7之前: false false
    // JDK 7之后: false true
    

    调整 intern() 方法的位置:

    public static void main(String[] args) {
        String s = new String("1");
        /**
         * 堆中存在一个对象:
         * String("1")
         * 字符串常量池中存在一个对象:
         * String("1")
         * 将堆中的String("1")的引用返回
         */
        String s2 = "1";
        /**
         * 显示声明,先看字符串常量池中是否拥有该对象(或者该对象的引用)
         * 存在:直接将其引用返回
         * 不存在:直接在字符串常量池中创建并返回该对象的引用
         * 此时存在String("1"),直接将其引用返回
         */
        s.intern();
        // 字符串常量池中存在,不作操作
        System.out.println(s == s2);
    
        String s3 = new String("1") + new String("1");
        /**
         * 堆中存在三个对象:
         * 一个StringBuilder("11")对象,两个String("1","11")对象
         * 字符串常量池中存在一个对象:
         * String("1")
         * 将堆中的String("11")的引用返回
         */
        String s4 = "11";
         /**
         * 显示声明,先看字符串常量池中是否拥有该对象(或者该对象的引用)
         * 存在:直接将其引用返回
         * 不存在:直接在字符串常量池中创建并返回该对象的引用
         * 此时由于还未调用intern()方法,所以直接在字符串常量池中创建String("11")对象,并将其引用返回
         */
        s3.intern();
        // 字符串常量池中存在,不作操作
        System.out.println(s3 == s4);
        /**
         * 创建对象完成后才调用intern()方法,此时字符串常量池中已经存在对象,所以没有影响
         * 一个是堆中对象的引用,一个是字符串常量池中对象的引用
         */
    }
    
    // JDK 7之前: false false
    // JDK 7之后: false false
    
1.7 字符串的比较、拼接与分割
1.7.1 比较
  • == 操作符用于比较两个对象的地址是否相等

  • equals() 方法用于比较两个对象的内容是否相等

    // Object 类的equals 源码
    public boolean equals(Object obj) {
        return (this == obj);
    }
    

    Object 类的 equals() 方法默认采用的是 == 操作符进行比较。假如子类没有重写该方法的话,那么 == 操作符和 equals() 方法的功效就完全一样 – > 比较两个对象的内存地址是否相等

    // JDK 8下的 String.equals() 源码
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
    
    // JDK 11下的 String.equals() 源码
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (coder() == aString.coder()) {
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                        : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
    }
    

    如果两个字符串对象可以 == ,那就直接返回 true

    • (JDK 8)否则判断其类型是否为 String 类型,将其强制转换为 String 类型,然后将其值赋予 char[] 数组,然后挨个比较两个字符数组值是否相等,只要有一个不相等就返回false

    • (JDK 11) 否则判断其类型是否为 String 类型,将其强制转换为 String 类型,判断其编码类型,调用对应的编码其下的equals() 方法,将对应的字符串内容转化为 byte[] 数组,然后挨个比较两个字节数组值是否相等,只要有一个不相等就返回false

1.7.2 拼接

append()+concat()join()

循环体内,拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符

  • String 字符串是不可变对象,只要对其进行相关操作就会让其发生变化,就会重新生成新对象,这样在循环时会增加很多不必要的开销

  • StringBuilder 是可变对象,其下的 append() 方法就是直接在建好的对象后面直接添加,不会新生成对象,而且其速度很快

  • 简单说明:

    • StringBuilder 在创建对象时通过构造函数初始化,若未初始化则默认初始长度为16
    • value[] : 用来存储StringBuilder对象中的字符
    • count: 记录 StringBuilder 对象中所存字符的实际数量,即实际内容长度
    • Stringbuilder对象将所存储的内容存在value数组中,并通过count来记录内容的长度,所以count始终小于等于value数组的长度
    // StringBuilder.append() 源码
    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
    
    /**
     * super.append() 其父类的方法
     * AbstractStringBuilder.append() 源码
     * 
     */
    
    /**
     * 参数 String 的字符被追加的长度增加此序列的长度参数
     * 如果 str 是 null,那么四个字符"n","u","l","l"被追加
     * 否则调用ensureCapacityInternal() 方法以及 getChars
     */
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        // 将拼接的字符串 str 复制到目标数组 value 中
        str.getChars(0, len, value, count);
        // 更新数组的长度 count
        count += len;
        return this;
    }
    
    /**
     * 如果 str 是 null,那么四个字符"n","u","l","l"被追加,然后返回
     */
    private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }
    
    /**
     * minimumCapacity : 实际内容长度 + 添加的字符串内容长度
     * 如果该长度大于存储 StringBuilder 对象字符的内容长度大小
     * 则需要重新对 value(StringBuilder对象的字符内容) 进行赋值
     * Arrays.copyOf() 拷贝一份之前的 value ,然后追加一部分空间
     * 追加空间调用 newCapacity() 方法
     */
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                                  newCapacity(minimumCapacity));
        }
    }
    
    /**
     * minCapacity : minimumCapacity(实际内容长度 + 添加的字符串内容长度)
     * 此处的 value 为还没有重新赋值的 StringBuilder 对象的字符内容
     * newCapacity : 新空间大小
     * 如果 newCapacity 小于等于0或者空间最大值都小于 newCapacity
     * 此时调用 hugeCapacity
     */
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }
    
    /**
     * 如果 minCapacity 大于空间容量最大值,则抛出异常 OutOfMemoryError
     */
    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }
    
    
  • concat() 方法相较于 +

    // concat 源码
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }
    

    在遇到字符串为 null 的时候,会抛出 NullPointerException

    如果拼接的字符串是一个空字符串 ("") ,那么 concat 的效率要更高一点

    如果拼接的字符串非常多,concat() 的效率就会下降,因为创建的字符串对象越来越多

  • join() 静态方法

    // join 源码
    public static String join(CharSequence delimiter, CharSequence... elements) {
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        // Number of elements not likely worth Arrays.stream overhead.
        StringJoiner joiner = new StringJoiner(delimiter);
        for (CharSequence cs: elements) {
            joiner.add(cs);
        }
        return joiner.toString();
    }
    

    该静态方法里面新建了一个叫 StringJoiner 的对象,然后通过 ~循环把可变参数添加了进来,最后调用 toString() 方法返回 String

    实际的工作中,org.apache.commons.lang3.StringUtilsjoin() 方法也经常用来进行字符串拼接

    // 源码
    public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
        if (array == null) {
            return null;
        }
        if (separator == null) {
            separator = EMPTY;
        }
    
        final StringBuilder buf = new StringBuilder(noOfItems * 16);
    
        for (int i = startIndex; i < endIndex; i++) {
            if (i > startIndex) {
                buf.append(separator);
            }
            if (array[i] != null) {
                buf.append(array[i]);
            }
        }
        return buf.toString();
    }
    
    • 该方法不用担心 NullPointerException
    StringUtils.join(null)  // null          
    StringUtils.join([])    // ""
    StringUtils.join([null])   // ""
    StringUtils.join(["a", "b", "c"]) // "abc"
    StringUtils.join([null, "", "a"]) // "a"
    
1.7.3 分割

split()

  • 在拆分之前,要先进行检查,判断一下这串字符是否包含分割内容,否则应该抛出异常

    public class Test {
        public static void main(String[] args) {
            String cmower = "Hello, world!";
            if (cmower.contains(",")) {
                String [] parts = cmower.split(",");
                System.out.println("第一部分:" + parts[0] +" 第二部分:" + parts[1]);
            } else {
                throw new IllegalArgumentException("当前字符串没有包含逗号");
            }
        }
    }
    
    // 第一部分:Hello  第二部分:world!
    
  • 在字符串是确定的情况下,最重要的是分隔符是确定的,否则就会抛出异常

  • 一般情况下都是使用已经确定的字符进行分割,所以很少会使用到正则表达式进行分割

  • 使用split()方法时,一定要注意该字符串是否包含特殊字符,而且需要注意的是:

    • 当特殊字符为 . 或者 | 时需要添加转义字符 --> split("\\.") 或者split("\\|")
  • 遇到特殊符号使用正则表达式(GitHub上开源正则学习文档)(一组由字母和符号组成的特殊文本,它可以用来从文本中找出满足你想要的格式的句子)

StringTokenizer

JDK中提供的专门用来处理字符串分割子串的工具类

public class Test {
    public static void main(String[] args) {
        String str01 = "Hello world!";
        StringTokenizer str02 = new StringTokenizer(str01);
        while (str02.hasMoreElements()){
            System.out.println(str02.nextElement());
        }
    }
}
// Hello
// world!
  • StringTokenizer 常用的函数:

    • hasMoreElements()hasMoreTokens()

      这两个方法用法一样,功能也一样,表示StringTokenizer是否还有元素,当其为False,表示StringTokenzier为空

    • nextElement()nextToken()

      这两个方法的用法是一样的,返回此 StringTokenizer 的下一个标记

    • int countTokens()

      返回nextToken()方法被调用的次数。

  • StringTokenizer有三个构造函数:

    • StringTokenizer(String str)

      默认以" \t\n\r\f"(前有一个空格,引号不是)为分割符,该方法默认以空格、回车、换行等为分隔符,默认不返回分隔符

    • StringTokenizer(String str, String delim)

      指定 delim 为分割符,默认不返回分隔符

      // 实例
      public class Test {
          public static void main(String[] args) {
              String str01 = "HelloWorld!";
              StringTokenizer str02 = new StringTokenizer(str01, "o");
              while (str02.hasMoreElements()){
                  System.out.println(str02.nextElement());
              }
          }
      }
      
      // Hell
      // W
      // rld!
      // 
      
    • StringTokenizer(String str, String delim, boolean returnDelims)

      returnDelimstrue 时,默认返回分隔符

      // 实例
      public class Test {
          public static void main(String[] args) {
              String str01 = "HelloWorld!";
              StringTokenizer str02 = new StringTokenizer(str01, "o", true);
              while (str02.hasMoreElements()){
                  System.out.println(str02.nextElement());
              }
          }
      }
      
      // Hell
      // o
      // W
      // o
      // rld!
      // 
      

    subString()indexOf()

这两个方法组合起来可以分割字符

// 实例
public class Test {
    public static void main(String[] args) {
    	String str01 = "HelloWorld!";
        // 获取分隔符的索引:当字符串中有多个分割符时,以找到的第一个分割符为主
       	int start01 = str01.indexOf("o");
        String str02 = str01.subString(start01);
        int start02 = str02.indexOf("o");
        // 分割号的字符串子串
        String str03 = str01.subString(0, start01);
        String str04 = str01.subString(start01, start02);
        String str05 = str01.subString(start02);
    }
}
  • 注意:该方法不适合用来分割,仅仅介绍有这种方法

有任何错误请指正,谢谢您的阅读,希望对您有所帮助。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值