【JDK8源码】java.lang.String类阅读笔记

【JDK8源码】java.lang.String类阅读笔记

前言

个人见解,有理解错误的地方望指正。

概述

Java中用来创建和操作字符串的类,一个不可变的类。

类的定义

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
  • 刚刚上面说到String是一个不可变的类,这里可以看到String类是用final修饰的,是一个不可继承不可变的类
  • 实现了Serializable接口,表示这个类可以被序列化
  • 实现Comparable接口,默认的比较器,用来比较两个字符的大小,实现了这个接口意味着类支持排序(按照实现方法内的规则排序),可以使用Collections.sort()或者Arrays.sort()等方法。
  • 实现了CharSequence接口,表示是一个有序的字符集合,提供了对序列字符的一些操作。

属性

    //存储字符串的数组
    //也是使用final修饰的,所以String还是不可变的,不可变只是指数组的引用不可变
    //数组是一段连续的内存地址,里面的某个值还是可以变的
    private final char value[];
    //缓存字符串的hash代码
    private int hash; 
    //序列化编号
    private static final long serialVersionUID = -6849794470754667710L;
    //也是用来参与序列化的一个属性
    private static final ObjectStreamField[] serialPersistentFields =new ObjectStreamField[0];

常见构造方法

  • String()
   //无参构造函数,创建一个空的字符串对象
   public String() {
       this.value = "".value;
   }
  • String(String original)
    //入参一个String类型,将String的value和hash都赋值给新的String
   public String(String original) {
       this.value = original.value;
       this.hash = original.hash;
   }
  • String(char value[])
   //入参一个char类型的数组,使用Arrays.copy()将参数数组的长度和内容赋值给新的该String的value
   public String(char value[]) {
       this.value = Arrays.copyOf(value, value.length);
   }
  • String(char value[], int offset, int count)
   //入参一个char类型数组,将数组从下标offset开始复制count个,再赋值String的value
    public String(char value[], int offset, int count) {
      //如果起始下标小于0,抛出异常
       if (offset < 0) {
           throw new StringIndexOutOfBoundsException(offset);
       }
       if (count <= 0) {
       //如果复制长度小于0,抛出异常
           if (count < 0) {
               throw new StringIndexOutOfBoundsException(count);
           }
           //如果复制长度=0,且起始下标<=数组长度,创建一个空的数组
           if (offset <= value.length) {
               this.value = "".value;
               return;
           }
       }
       // 如果起始下标大于数组长度-数组长度,抛出异常
       if (offset > value.length - count) {
           throw new StringIndexOutOfBoundsException(offset + count);
       }
       //调用copyOfRange方法复制数组
       this.value = Arrays.copyOfRange(value, offset, offset+count);
   }
  • String(byte bytes[], int offset, int length, String charsetName)
//从bytes[]数组的offset位置截取长度为length的字符,以charsetName编码复制给String的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);
   }
  • String(StringBuffer buffer)
  //入参一个String Buffer对象,将buffer的长度和内容复制给Sting的vlaue数组
    public String(StringBuffer buffer) {
       synchronized(buffer) {
           this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
       }
   }
  • String(StringBuilder builder)
   //和上面一样
       public String(StringBuilder builder) {
       this.value = Arrays.copyOf(builder.getValue(), builder.length());
   }

常用方法

  1. length():
//返回当前字符串长度,构造比较简单,直接返回了value数组的长度
 public int length() {
       return value.length;
   }
  1. isEmpty():
//判断字符串是否为空,判断value的长度是否等于0,如果为0则返回false,否则返回true
 public boolean isEmpty() {
        return value.length == 0;
    }
  1. charAt(int index):
   //获取字符串中的index索引处的值
    public char charAt(int index) {
    //判断索引值是否超过数组长度,超过则抛出异常
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        //否则返回数组中索引对应的值
        return value[index];
    }
  1. equals:
  • equals(Object object):
//将字符串和指定的对象作比较,如果相等返回true,否则返回false
   public boolean equals(Object anObject) {
       //先直接用==判断,即两个对象指向的引用地址是否是同一个,如果是直接返回true
        if (this == anObject) {
            return true;
        }
        //判断是否是String类型的
        if (anObject instanceof String) {
        //转换为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])
                    //只要有一个不相等就返回false
                        return false;
                    i++;
                }
                //否则返回true
                return true;
            }
        }
        //如果不是String类型直接返回false
        return false;
    }
  • equalsLgnoreCase(String value):
    //忽略大小写,将String与另一个String字符串作比较
    public boolean equalsIgnoreCase(String anotherString) {
    //如果指向同一地址,直接返回true,否则就判断字符串不为空,且两个字符串长度相同且字符相同
        return (this == anotherString) ? true
                : (anotherString != null)
                && (anotherString.value.length == value.length)
                && regionMatches(true, 0, anotherString, 0, value.length);
    }
    /**
    ignoreCase:是否忽略大小写
    tooffset:当前字符串起始位置
    other:要比较的字符串
    ppffset:参数字符的起始位置
    len:要比较的字符数
    **/
 public boolean regionMatches(boolean ignoreCase, int toffset,
            String other, int ooffset, int len) {
        char ta[] = value;
        int to = toffset;
        char pa[] = other.value;
        int po = ooffset;
        // 先判断起始位置是否正确
        if ((ooffset < 0) || (toffset < 0)
                || (toffset > (long)value.length - len)
                || (ooffset > (long)other.value.length - len)) {
            return false;
        }
        //循环判断字符是否相等
        while (len-- > 0) {
            char c1 = ta[to++];
            char c2 = pa[po++];
            //如果==判断为真,直接跳出循环进入下一次循环
            if (c1 == c2) {
                continue;
            }
            //否则判断是否需要忽略大小写
            if (ignoreCase) {
                //将字符转换为大写,再做比较
                char u1 = Character.toUpperCase(c1);
                char u2 = Character.toUpperCase(c2);
                if (u1 == u2) {
                    continue;
                }
                //转换为小写作比较
                if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
                    continue;
                }
            }
            //不满足返回false
            return false;
        }
        return true;
    }
  1. compareTo
  • compareTo(String str)
//按字典顺序比较两个字符串
//如果当前字符串等于参数字符串返回0
//如果当前字符串小于参数字符串,返回负数
//如果当前字符串大于参数字符串,返回正数
  public int compareTo(String anotherString) {
        //获取当前字符串的长度
        int len1 = value.length;
        //获取要比较的字符串的长度
        int len2 = anotherString.value.length;
        //获取两个长度之间的最小值
        int lim = Math.min(len1, len2);
        //获取当前字符串的字符数组
        char v1[] = value;
        //获取参数字符串的字符数组
        char v2[] = anotherString.value;
        int k = 0;
        //循环判断
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            //如果两个字符不相等,返回两个字符在字母表中的顺序
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        //如果都相等,直接返回长度的差值
        return len1 - len2;
    }
  • compareToLgnoreCase(String str)
    //忽略大小写比较两个字符串的大小,返回值与上面的方法一致
    public int compareToIgnoreCase(String str) {
    //调用了内部类CaseInsensitiveComparator的compare方法
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }
    public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
    private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
        //和获取两个字符数组的长度,和两个数之间的最小值
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                //循环判断
                //先直接==判断
                if (c1 != c2) {
                //转成大写判断 
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                    //转成小写判断
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            //返回字母表顺序差值
                            return c1 - c2;
                        }
                    }
                }
            }
            //直接返回长度差值
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }
  1. hashCode():
//返回字符串的哈希码
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
  1. indexOf():
  • indexOf(String ch,int index)
//返回指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1
  public int indexOf(String str, int fromIndex) {
        return indexOf(value, 0, value.length,
                str.value, 0, str.value.length, fromIndex);
    }
    /**
    source 当前字符串字符数组
    sourceOffset 当前字符串起始位置
    sourceCount 当前字符串长度
    target 参数字符串字符数组
    targetOffset 参数字符串起始位置
    targetCount 参数字符串长度
    fromIndex 要开始查找的索引
    **/
   static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) { 
            //如果开始查找的索引>当前字符串长度
        if (fromIndex >= sourceCount) {
        //判断参数字符串长度是否等于0,如果为0则返回当前字符串的长度,否则返回-1
            return (targetCount == 0 ? sourceCount : -1);
        }
        //如果索引<0则从0开始查找
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        //如果查找的字符串为空,则返回formindex
        if (targetCount == 0) {
            return fromIndex;
        }
        //找到参数字符串中的第一个字符,标记为first
        char first = target[targetOffset];
        //获取当前字符串应该要查找的最大长度
        int max = sourceOffset + (sourceCount - targetCount);
       //循环查找
        for (int i = sourceOffset + fromIndex; i <= max; i++) {
           // 匹配参数字符串的第一个字符,在当前字符串的最大长度内没有匹配到就直接跳出,不会执行后面的代码
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }

            //如果已经跳出了while循环且i<max,说明匹配到了参数字符串的第一个字符
            if (i <= max) {
                 //得到第二个字符要匹配的位置
                int j = i + 1;
                //计算出接下来还要匹配的字符最大长度
                int end = j + targetCount - 1;
                //循环判断,从参数字符串的第二个字符、当前字符串的i+1位置开始判断后面的每一个字符是否都相等
                //这里有一个条件,满足当前字符串j处的索引值==参数字符串的起始位置+1处索引值,k和j才会自增,匹配下一个字符
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);
                //当j自增到最大长度,说明所有字符都匹配成功
                //如果没有到最大长度,说明有字符没有匹配到,此时跳出进入下一轮for循环继续匹配
                if (j == end) {
                    //返回索引位置
                    return i - sourceOffset;
                }
            }
        }
        //没有匹配到,返回-1
        return -1;
    }

  • indexOf(String ch)
//内部调用了上面的方法
  public int indexOf(String str) {
        return indexOf(str, 0);
    }
  1. lastIndexOf
  • lastIndexOf(String str,int formIndex):
 //查询参数字符串在当前字符串中最后出现的索引位置,如果找到了返回索引位置,如果没有找到返回-1
    public int lastIndexOf(String str, int fromIndex) {
        return lastIndexOf(value, 0, value.length,
                str.value, 0, str.value.length, fromIndex);
    }
  /**
  source:当前字符串的字符数组
  sourceOffset:当前字符串起始索引值
  sourceCount:当前字符串长度
  target:参数字符串字符数组
  targetOffset:参数字符串起始位置索引值
  targetCount:参数字符串长度
  fromIndex:从formIndex开始查找
  **/
  static int lastIndexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) {
       //算出要查找的最末尾(最大索引值)
        int rightIndex = sourceCount - targetCount;
        //如果开始查找的索引位置小于0,直接返回-1
        if (fromIndex < 0) {
            return -1;
        }
        //如果查找的起始位置>最末尾索引值,则直接等于最末尾的索引值
        if (fromIndex > rightIndex) {
            fromIndex = rightIndex;
        }
        //如果参数字符串为空
        if (targetCount == 0) {
        //直接返回开始查找的索引值
            return fromIndex;
        }
        //获取参数字符串最后一个字符的索引值
        int strLastIndex = targetOffset + targetCount - 1;
        //获取参数字符串的最后一个索引值,标志为strLastChar
        char strLastChar = target[strLastIndex];
        //计算出当前字符串要匹配参数字符串的最小索引值
        int min = sourceOffset + targetCount - 1;
        //计算开始查找的索引位置
        int i = min + fromIndex;
       //跳出循环的标记号
       startSearchForLastChar:
        while (true) {
          //循环匹配最后一个字符
            while (i >= min && source[i] != strLastChar) {
                i--;
            }
            //如果没有匹配到最后一个字符或者是匹配到了但是索引已经小于了最小索引,直接返回-1
            if (i < min) {
                return -1;
            }
            //下一个字符开始匹配的位置
            int j = i - 1;
            //计算参数字符串中第一个字符的位置-1,用来控制下一次循环次数
            int start = j - (targetCount - 1);
            //参数字符的倒数第二个字符的查询位置
            int k = strLastIndex - 1;
            //循环递减匹配当前字符串的j处索引值和参数字符串的k处索引值是否一致
            while (j > start) {
                if (source[j--] != target[k--]) {
                    i--;//第一次进来的时候是字符串的最后一个字符匹配位置,所以要递减匹配的每一个字符,用来控制下一次的循环从哪里开始匹配
                    //如果不一致,跳出到循环外面的startSearchForLastChar标记处
                    continue startSearchForLastChar;
                }
            }
            //返回索引位置
            return start - sourceOffset + 1;
        }
    }
  • lastIndexOf(String str):
//内部调用了上面的方法
 public int indexOf(String str) {
        return indexOf(str, 0);
    }
  1. substring:
  • substring(int index):
   //截取当前字符串,从beginIndex索引开始截取到最后一个索引处
    public String substring(int beginIndex) {
    //如果索引位置小于0,抛出异常
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        //计算要截取的字符串长度
        int subLen = value.length - beginIndex;
        //如果小于0,说明截取位置超过了数组的长度,抛出异常
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //如果截取位置=0,直接返回当前字符串,否则调用构造方法,返回一个新的字符串,构造方法上面已经介绍过
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
  • substring(int index,length):
//截取字符串,返回一个新的字符串,从beginIndex索引处开始截取,截取到endIndex索引处
 public String substring(int beginIndex, int endIndex) {
        //如果起始位置<0,抛出异常
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        //如果结束索引位置>数组的长度,抛出异常
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        //计算要截取的数组长度
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //如果
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }
  1. replace:
  • replace(char oldChar, char newChar)
    //把字符串中的oldChar字符替换成newChar字符并返回新的字符
    public String replace(char oldChar, char newChar) {
    //如果要替换的字符不等于新字符才进行替换
        if (oldChar != newChar) {
            //获取当前字符串长度
            int len = value.length;
            //起始下标
            int i = -1;
            //当前字符串的值
            char[] val = value; 
            //循环对比是否有需要替换的字符
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            //当i小于当前字符串长度时
            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;
    }
  • replaceAll(String regex, String replacement)
//传入一个正则表达式regex,将匹配regex正则的字符串替换为replacement字符串
 public String replaceAll(String regex, String replacement) {
//调用了三个方法
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }
    //java.util.regex.Pattern类的compile方法
    //解析正则表达式
  public static Pattern compile(String regex) {
        return new Pattern(regex, 0);
    }
    
  //java.util.regex.Pattern的matcher方法
  //获取匹配器
  public Matcher matcher(CharSequence input) {
        if (!compiled) {
            synchronized(this) {
                if (!compiled)
                    compile();
            }
        }
        Matcher m = new Matcher(this, input);
        return m;
    }
    //java.util.regex.Matcher类的replaceAll方法
    //替换字符串
   public String replaceAll(String replacement) {
        reset();
        //查找是否有需要替换的字符
        boolean result = find();
        if (result) {
            StringBuffer sb = new StringBuffer();
            do {
            //进入方法替换,内容有点长,具体实现下次阅读Matcher类源码的时候再研究
                appendReplacement(sb, replacement);
                result = find();
            } while (result);
            appendTail(sb);
            return sb.toString();
        }
        return text.toString();
    }
  1. split:
  • split(String regex, int limit)
  //根据指定的正则表达式regex,将字符串进行拆分成limit份
    public String[] split(String regex, int limit) {
        char ch = 0;
        //第一种情况是如果regex只有一位,且不包含这些特殊字符
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 && //第二种情况是,如果regex长度是两位
              regex.charAt(0) == '\\' &&//且第一个字符为转义符
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&//第二个字符不为数字、字母
            (ch < Character.MIN_HIGH_SURROGATE ||//utf-16 编码中的 unicode 高代理项代码单元的最小值。高代理项也称为前导代理项。
             ch > Character.MAX_LOW_SURROGATE))//utf-16 编码中的 unicode 高代理项代码单元的最大值。
        {
            int off = 0;//记录下一次匹配的位置
            int next = 0;//匹配到的位置,如果等于-1说明后面已经没有了
            boolean limited = limit > 0;
            ArrayList<String> list = new ArrayList<>();
            //循环匹配要分割的字符
            while ((next = indexOf(ch, off)) != -1) {
            //循环添加去掉分割符号后的每一个字符串到list中
                if (!limited || list.size() < limit - 1) {
                    list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
           //如果没有匹配到,直接把当前字符串转换为一个数组返回
            if (off == 0)
                return new String[]{this};

            // 刚刚的循环中如果没有添加最后一个分隔符后面的字符串,这里要添加
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));

            //获取lisi的长度
            int resultSize = list.size();
            //截取list集合最后面的空字符串
            if (limit == 0) {
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                    resultSize--;
                }
            }
            //创建一个新的数组
            String[] result = new String[resultSize];
            //把集合转换为数组返回
            return list.subList(0, resultSize).toArray(result);
        }
        //如果上面两个条件都不满足,要调用Pattern类的方法,下次阅读Pattern时再做研究
        return Pattern.compile(regex).split(this, limit);
    }
  • split(String regex)
   //内部调用了上面的方法
    public String[] split(String regex) {
        return split(regex, 0);
    }
  1. 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--;
        }
        //如果st大于0或者len小于0说明有空格,返回截取的字符串,否则返回原本的字符串
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
    }
  1. toString():
    //返回对象本身,已经是一个字符串
    public String toString() {
        return this;
    }
  1. toCharArray():
//将字符串转换为一个char数组返回
    public char[] toCharArray() {
        // 创建一个等长的数组
        char result[] = new char[value.length];
        //将原数组复制到新的数组在再返回
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
  1. valueOf():
//将Object对象转换为String对象返回
    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }
  1. intern():
//标志为native的方法,底层使用其他语言编写
//主要作用(官方解释):
//当调用intern方法的时候,如果量池中不存在这个字符串的引用,将这个对象的引用加入常量池,返回这个对象的引用。当常量池中存在"abc"这个字符串的引用,返回这个对象的引用;
 public native String intern();

其他问题

compareToLgnoreCase()方法和equalsLgnoreCase()中在转换大小写判断的时候为什么转成大写判断后还要再转成小写判断??

  • API说明中有写道:public static char toUpperCase(char ch)使用来自 UnicodeData 文件的大小写映射信息将字符参数转换为大写。
    注意,对于某些范围内的字符,特别是那些是符号或表意符号的字符,Character.isUpperCase(Character.toUpperCase(ch)) 并不总是返回 true。
    通常,应该使用 String.toUpperCase() 将字符映射为大写。String 大小写映射方法有几个胜过 Character 大小写映射方法的优点。String 大小写映射方法可以执行语言环境敏感的映射、上下文相关的映射和 1:M 字符映射,而 Character 大小写映射方法却不能。
    注:此方法无法处理增补字符。若要支持所有 Unicode 字符,包括增补字符,请使用 toUpperCase(int) 方法。

String为什么要设计成不可变的?

  • 线程安全问题,多线程并发的情况下,多个线程同时对一个资源进行写的操作会造成线程安全问题,String类是不可变的对象,不能被写,所以线程安全,而且String类在Java中的使用非常频繁,比如网络连接地址、用户密码、文件路径、类加载中的类名等字符串之类的,设计成不可变的,提高了安全性。
  • 同一个字符串指向的是常量池中的同一引用地址,节省了一定的空间。
  • 再具体一点的可以参考另一个博主的文章:https://blog.csdn.net/jiahao1186/article/details/81150912
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值