zoukankan      html  css  js  c++  java
  • java基础进阶一:String源码和String常量池

    作者:NiceCui

    1. String 介绍,常用方法源码分析

    2. String 常量池分析

    • 常用方法

    1. equals

    2. trim

    3. replace

    4. concat

    5. split

    6. startsWith 和 endsWith

    7. substring

    8. toUpperCase() 和 toLowerCase()

    9. compareTo

    • String 介绍

    String类被final所修饰,也就是说String对象是不可变量,并发程序最喜欢不可变量了。String类实现了Serializable, Comparable, CharSequence接口。

    从一段代码说起:

    public void stringTest(){
        String a = "a"+"b"+1;
        String b = "ab1";
        System.out.println(a == b);
    }
    

    大家猜一猜结果如何?如果你的结论是true。好吧,再来一段代码:

    public void stringTest(){
        String a = new String("ab1");
        String b = "ab1";
        System.out.println(a == b);
    }
    

    结果如何呢?正确答案是false。

    让我们看看经过编译器编译后的代码如何

    //第一段代码
    public void stringTest() {
        String a = "ab1";
        String b = "ab1";
        System.out.println(a == b);
    }
    
    //第二段代码
    public void stringTest() {
        String a1 = new String("ab1");
        String b = "ab1";
        System.out.println(a1 == b);
    }
    

    也就是说第一段代码经过了编译期优化,原因是编译器发现"a"+"b"+1和"ab1"的效果是一样的,都是不可变量组成。但是为什么他们的内存地址会相同呢?如果你对此还有兴趣,那就一起看看String类的一些重要源码吧。

    • 源码

    一、 String属性

    String类中包含一个不可变的char数组用来存放字符串,一个int型的变量hash用来存放计算后的哈希值。

    /** The value is used for character storage. */
    private final char value[];
    
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
    

    二、 String构造函数

    //不含参数的构造函数,一般没什么用,因为value是不可变量
    public String() {
        this.value = new char[0];
    }
    
    //参数为String类型
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    
    //参数为char数组,使用java.utils包中的Arrays类复制
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    
    //从bytes数组中的offset位置开始,将长度为length的字节,以charsetName格式编码,拷贝到value
    public String(byte bytes[], int offset, int length, String charsetName)
            throws UnsupportedEncodingException {
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(charsetName, bytes, offset, length);
    }
    
    //调用public String(byte bytes[], int offset, int length, String charsetName)构造函数
    public String(byte bytes[], String charsetName)
            throws UnsupportedEncodingException {
        this(bytes, 0, bytes.length, charsetName);
    }
    

    三、 String常用方法

    1. equals

    boolean equals(Object anObject)
    
    public boolean equals(Object anObject) {
        //如果引用的是同一个对象,返回真
        if (this == anObject) {
            return true;
        }
        //如果不是String类型的数据,返回假
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = value.length;
            //如果char数组长度不相等,返回假
            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;
    }
    
    String e1 = "good";
    String e2 = "good everyDay";
    e1.equals(e2); // 返回 false
    
    • 1 首先判断是否 引用同一个对象 == 也就是判断 这两个引用的 内存地址是否相同,如果相同 直接返回 true

    • 2 会判断是否类型 相同,是否是同一种数据类型

    • 3 类型 相同 就会比较 转换成的 字符 数组的长度 是否相同

    • 4 从后往前 比较 每一个字符 是否 相同

    • 判断顺序 =》 1.内存地址 2.数据类型 3.字符数组长度 4.单个字符比较

    2. compareTo

    int compareTo(String anotherString)
    
    public int compareTo(String anotherString) {
        //自身对象字符串长度len1
        int len1 = value.length;
        //被比较对象字符串长度len2
        int len2 = anotherString.value.length;
        //取两个字符串长度的最小值lim
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;
    
        int k = 0;
        //从value的第一个字符开始到最小长度lim处为止,如果字符不相等,返回自身(对象不相等处字符-被比较对象不相等字符)
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        //如果前面都相等,则返回(自身长度-被比较对象长度)
        return len1 - len2;
    }
    
    String co1 = "hello" ;
    String co2 = "hello";
    String co3 = "hello you";
    		 
    System.out.println(co1.compareTo(co2)); // 0
    System.out.println(co1.compareTo(co3)); // -4
    
    • 这个方法写的很巧妙,先从0开始判断字符大小。
    • 如果两个对象能比较字符的地方比较完了还相等,就直接返回自身长度减被比较对象长度,如果两个字符串长度相等,则返回的是0,巧妙地判断了三种情况。

    3.hashCode

    int hashCode()
    
    public int hashCode() {
        int h = hash;
        //如果hash没有被计算过,并且字符串不为空,则进行hashCode计算
        if (h == 0 && value.length > 0) {
            char val[] = value;
    
            //计算过程
            //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            //hash赋值
            hash = h;
        }
        return h;
    }
    
    String a = "toyou";
    char val[] = a.toCharArray();
    char c1 = 't';
    char c2 = 'a';
    int f = c1; 
    int e = c2;
    		 
    System.out.println(e); 		     // 97   a
    System.out.println(f);			 // 116  t
    System.out.println(31*val[0]);   // 3596
    System.out.println(31*c1);		 // 3596
    
    // hashCode 计算中 因为char 字符可以自动转换成对应的 int 整形
    
    • String类重写了hashCode方法,Object中的hashCode方法是一个Native调用。
    • String类的hash采用多项式计算得来,我们完全可以通过不相同的字符串得出同样的hash,所以两个String对象的hashCode相同,并不代表两个String是一样的。
    • 同一个String 对象 hashCode 一定相同, 但是 hashCode相同 ,不一定是同一个对象

    4.startsWith

    boolean startsWith(String prefix,int toffset)
    
    public boolean startsWith(String prefix, int toffset) {
        char ta[] = value;
        int to = toffset;
        char pa[] = prefix.value;
        int po = 0;
        int pc = prefix.value.length;
        // Note: toffset might be near -1>>>1.
        //如果起始地址小于0或者(起始地址+所比较对象长度)大于自身对象长度,返回假
        if ((toffset < 0) || (toffset > value.length - pc)) {
            return false;
        }
        //从所比较对象的末尾开始比较
        while (--pc >= 0) {
            if (ta[to++] != pa[po++]) {
                return false;
            }
        }
        return true;
    }
    
    public boolean startsWith(String prefix) {
        return startsWith(prefix, 0);
    }
    
    public boolean endsWith(String suffix) {
        return startsWith(suffix, value.length - suffix.value.length);
    }
    
     String d = "www.58fxp.com";
    			
     System.out.println(d.startsWith("www")); // true
     System.out.println(d.endsWith("com"));	  // true
    
    • 起始比较和末尾比较都是比较经常用得到的方法,例如在判断一个字符串是不是http协议的,或者初步判断一个文件是不是mp3文件,都可以采用这个方法进行比较。

    5.concat

    String concat(String str)
    
    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);
    }
    
    String cat = "much";
    		 
    String newcat = cat.concat(" yes"); // much yes
    
    • concat方法也是经常用的方法之一,它先判断被添加字符串是否为空来决定要不要创建新的对象。
    • 1 如果 拼接的字符 长度为0 直接返回 原字符对象
    • 2 拼接的字符 不为空 返回 新的 字符对象
    • 判断字符长度 生成新对象

    6.replace

    String replace(char oldChar,char newChar)
    
    public String replace(char oldChar, char newChar) {
        //新旧值先对比
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */
    
            //找到旧值最开始出现的位置
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            //从那个位置开始,直到末尾,用新值代替出现的旧值
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }
    
    String r1 = "how do you do";
    		 
    String r2 = r1.replace("do","is");
    System.out.println(r2); // how is you is
    
    • 这个方法也有讨巧的地方,例如最开始先找出旧值出现的位置,这样节省了一部分对比的时间。
    • replace(String oldStr,String newStr)方法通过正则表达式来判断。

    7.trim

    String trim()
    
    public String trim() {
        int len = value.length;
        int st = 0;
        char[] val = value;    /* avoid getfield opcode */
    
        //找到字符串前段没有空格的位置
        while ((st < len) && (val[st] <= ' ')) {
            st++;
        }
        //找到字符串末尾没有空格的位置
        while ((st < len) && (val[len - 1] <= ' ')) {
            len--;
        }
        //如果前后都没有出现空格,返回字符串本身
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
    }
    
    String t1 = " public void "; // 前后各一个空格
    		 
    System.out.println("t1:"+t1.length()); // 13 带空格长度
    		 
    String t2 = t1.trim();
    		 
    System.out.println("t2:"+t2.length()); // 11 去掉空格
    		 
    System.out.println(t2);
    

    8.intern

    String intern()
    
    public native String intern();
    
    String dd = new String("bb").intern();
    
    • intern方法是Native调用,它的作用是在方法区中的常量池里通过equals方法寻找等值的对象,
    • 如果没有找到则在常量池中开辟一片空间存放字符串并返回该对应String的引用,否则直接返回常量池中已存在String对象的引用。
    • 可以为new方法创建的 字符对象 也去强制查看常量池 是否已存在

    将引言中第二段代码

    //String a = new String("ab1");
    //改为
    String a = new String("ab1").intern();
    
    • 则结果为为真,原因在于a所指向的地址来自于常量池,而b所指向的字符串常量默认会调用这个方法,所以a和b都指向了同一个地址空间。
    int hash32()
    
    private transient int hash32 = 0;
    int hash32() {
        int h = hash32;
        if (0 == h) {
           // harmless data race on hash32 here.
           h = sun.misc.Hashing.murmur3_32(HASHING_SEED, value, 0, value.length);
    
           // ensure result is not zero to avoid recalcing
           h = (0 != h) ? h : 1;
    
           hash32 = h;
        }
    
        return h;
    }
    
    • 在JDK1.7中,Hash相关集合类在String类作key的情况下,不再使用hashCode方式离散数据,而是采用hash32方法。
    • 这个方法默认使用系统当前时间,String类地址,System类地址等作为因子计算得到hash种子,通过hash种子在经过hash得到32位的int型数值。
    public int length() {
        return value.length;
    }
    public String toString() {
        return this;
    }
    public boolean isEmpty() {
        return value.length == 0;
    }
    public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
    }
    

    以上是一些简单的常用方法。

    总结

    • String对象是不可变类型,返回类型为String的String方法每次返回的都是新的String对象,除了某些方法的某些特定条件返回自身。

    • String对象的三种比较方式:

    • ==内存比较:直接对比两个引用所指向的内存值,精确简洁直接明了。

    • equals字符串值比较:比较两个引用所指对象字面值是否相等。

    • hashCode字符串数值化比较:将字符串数值化。两个引用的hashCode相同,不保证内存一定相同,不保证字面值一定相同。

    字符串常量池的设计思想

    一.字符串常量池设计初衷

    每个字符串都是一个String对象,系统开发中将会频繁使用字符串,如果像其他对像那样创建销毁将极大影响程序的性能。

    • JVM为了提高性能和减少内存开销,在实例化字符串的时候进行了优化
    1. 为字符串开辟了一个字符串常量池,类似于缓存区
    2. 创建字符串常量时,首先判断字符串常量池是否存在该字符串
    3. 存在该字符串返回引用实例,不存在,实例化字符串,放入池中
    • 实现基础
    1. 实现该优化的基础是每个字符串常量都是final修饰的常量,不用担心常量池存在数据冲突
    2. 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收

    堆、栈、方法区

    了解字符串常量池,首先看一下 堆栈方法区

    image

    1. 存储的是对象,每个对象都包含一个与之对应的class
    2. JVM只存在一个堆区,被所有线程共享,堆中不存在基本类型和对象引用,只存在对象本身
    3. 对象由垃圾回收器负责回收,因此大小和生命周期不需要确定
    1. 每个线程都包含一个栈区,栈区只存放基础数据类型对象和自定义对象引用
    2. 每个栈中的数据(原始类型和对象引用)都是私有的
    3. 栈分为三个部分,基本类型变量区、执行环境上下文、操作指令区(存放操作指令)
    4. 数据大小和生命周期是可以确定的,当没有引用指向这个数据时,这个数据就会消失
    • 方法区
    1. 静态区,跟堆一样,被所有的线程共享
    2. 方法区包含的都是在整个程序中永远唯一的元素,如class、static变量;

    字符串常量池

    字符串常量池存在于方法区

    代码:堆栈方法区存储字符串

    String str1 = “abc”;
    String str2 = “abc”;
    String str3 = “abc”;
    String str4 = new String(“abc”);
    String str5 = new String(“abc”);
    

    image

    面试题

    • String str4 = new String(“abc”) 创建多少个对象?
    1. 拆分: str4 = 、 new String()、"abc"
    2. 通过new 可以创建一个新的对象,new 方法创建实例化对象不会去常量池寻找是否已存在,只要new 都会实例化一个新的对象出来
    3. "abc"每个字符串 都是一个String 对象,如果常量池中没有则会创建一个新对象放入常量池,否则返回对象引用
    4. 将对象地址赋值给str4,创建一个引用
    5. 所以,常量池中没有“abc”字面量则创建两个对象,否则创建一个对象,以及创建一个引用
    • String str1 = new String("A"+"B") ; 会创建多少个对象? String str2 = new String("ABC") + "ABC" ; 会创建多少个对象?
  • 相关阅读:
    iOS 苹果开发证书失效的解决方案(Failed to locate or generate matching signing assets)
    iOS NSArray数组过滤
    App Store2016年最新审核规则
    iOS 根据字符串数目,自定义Label等控件的高度
    iOS 证书Bug The identity used to sign the executable is no longer valid 解决方案
    Entity FrameWork 增删查改的本质
    EF容器---代理类对象
    Entity FrameWork 延迟加载本质(二)
    Entity FrameWork 延迟加载的本质(一)
    Entity FrameWork 增删查改
  • 原文地址:https://www.cnblogs.com/NiceCui/p/8046564.html
Copyright © 2011-2022 走看看