百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

Java源码分析-String类分析 javaspring源码

yuyutoo 2024-10-21 12:03 1 浏览 0 评论

文/小图灵视界

本头条号将会继续分析JDK8的源码,欢迎关注和收藏,也会将分析笔记开源。

位置:java.lang

String类是除了Object类外,最基础的类,最重要的类,在开发过程中使用最常用的类,也是使用最多的类。String类的定义如下:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence

String类由final修饰符修饰,是不可被子类继承的类,是不可改变的类。String类实现了Serializable、Comparable、CharSequence三个接口,实现Serializable接口,对象就可以进行序列化,实现Comparable接口,String类的对象之间就可以进行比较,CharSequence接口提供了如下方法:

//计算长度
int length();
//返回某个位置的字符
 char charAt(int index);
//返回从start到end位置的子CharSequence
CharSequence subSequence(int start, int end);
//输出的字符串
public String toString();

String实现CharSequence的上述所有接口方法,为String提供计算长度、定位字符串某个位置的字符、返回字符串的子字符串以及标准的输出字符。String类有如下几个重要的属性:

//保存字符串
private final char value[];
//缓存字符串的哈希码
private int hash; 
//用于指定哪些字段需要被默认序列化
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

String类底层本质是用char数组进行保存字符的,char数组value属性用来保存字符串,hash属性是哈希码的值,默认值为0,serialPersistentFields属性用于指定哪些字段需要被默认序列化。这些属性都是final修饰,不可改变,String类也没有提供获取和修改这些属性的方法。

常用的构造方法

//默认为空字符串
public String() {
     this.value = "".value;
}
//用字符创构建字符串
public String(String original) {
      this.value = original.value;
      this.hash = original.hash
}
//用char数组初始化
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
//用StringBuffer初始化
public String(StringBuffer buffer) {
      synchronized(buffer) {
          this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
      }
}
//用StringBuilder初始化
public String(StringBuilder builder) {
      this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

String类是不可变的类,在初始化的过程中都是采用副本进行赋值。String() 构造器是默认的构造器,用空字符串的副本进行初始化( this.value = "".value;),String(String original)构造器将字符串original的value和hash属性的副本赋值给新的字符串的value和hash属性。String(char value[])将char 数组的复制给this.value,是用 Arrays.copyOf方法复制的, Arrays.copyOf方法底层是新创建一个char数组,然后利用本地方法System.arraycopy将value数组的值复制给新创建char数组。

String(StringBuffer buffer) 构造函数将StringBuffer的char数组复制给字符串,这个构造器用synchronized对buffer加锁,原因是String类是不可变的,在多线程的环境下是安全的,但是StringBuffer类是不安全的,为了保证用StringBuffer初始化字符串时的安全性而加锁。最后一个常用的构造方法是String(StringBuilder builder),利用StringBuilder进行初始化,将StringBuilder的char数组复制给字符串,StringBuilder类是安全的。

用的方法

public int length() {
    return value.length;
}

length()方法返回字符串的长度,这个长度就是底层char数组的长度。

public boolean isEmpty() {
    return value.length == 0;
}

isEmpty()方法判断字符串的长度是否等于0,实际判断的是底层char数组的长度是否等于0。这个方法具有误导性,实际上并不是判空操作,在开发过程中判断一个字符串是否为空,常用if(str==null || str.length()==0)进行判空操作。

public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
}

charAt(int index) 方法返回给定位置index的字符,首先判断给定位置的index是否合法,如果合法,返回char数组value的index位置的字符,否则,返回StringIndexOutOfBoundsException异常。

void getChars(char dst[], int dstBegin) {
        System.arraycopy(value, 0, dst, dstBegin, value.length);
}

getChars(char dst[], int dstBegin)方法将字符串的value数组从dst数组的dstBegin位置复制到dst数组中。利用了 System.arraycopy本地方法复制。

public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        Objects.requireNonNull(dst);

        int j = dstBegin;
        int n = srcEnd;
        int i = srcBegin;
     	//重新复制一个char数组
        char[] val = value;   /* avoid getfield opcode */

        while (i < n) {
            //将char强转为byte
            dst[j++] = (byte)val[i++];
        }
}

getBytes方法将字符串的char数组复制给byte数组。srcBegin参数表示的从value数组的srcBegin位置,srcEnd参数表示的从value数组的srcEnd位置,复制srcBegin位置到srcEnd位置的字符。这个方法首先判断参数是否越界,getBytes方法并不是直接操作字符串中的chat数组value,而是复制了一个value的副本val,原因是避免getfield操作,最后用循环遍历副本val各个位置的值转为byte类型赋值给dst数组。

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;
}

String类重写了Object父类的equals方法,首先判断比较的对象anObject是否是这个对象this,如果是直接返回true,否则继续判断这个对象是否属于String类型,如果是的话,判断下anObject对象的value数组的长度是否等于这个字符串的value数组的长度,如果是的话,遍历两个比较对象的value数组中的每个位置的字符是否相等,如果相等返回true,否则就返回false。

equals方法比较巧妙,先判断是否是this和判断两个比较字符之间长度,这两个判断提高了快速判断两个字符串是否相等,只有这两个判断都成立了,才遍历比较两个字符相同位置的字符是否相等。

public boolean contentEquals(CharSequence cs) {
        // Argument is a StringBuffer, StringBuilder
        if (cs instanceof AbstractStringBuilder) {
            if (cs instanceof StringBuffer) {
                synchronized(cs) {
                   return nonSyncContentEquals((AbstractStringBuilder)cs);
                }
            } else {
                return nonSyncContentEquals((AbstractStringBuilder)cs);
            }
        }
        // Argument is a String
        if (cs instanceof String) {
            return equals(cs);
        }
        // Argument is a generic CharSequence
        char v1[] = value;
        int n = v1.length;
        if (n != cs.length()) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (v1[i] != cs.charAt(i)) {
                return false;
            }
        }
        return true;
}

contentEquals方法是判断字符序列(CharSequence)的内容与字符串的内容是否相等。传入的参数是CharSequence,首先判断传入的参数是否是AbstractStringBuilder类型,AbstractStringBuilder类型是StringBuffer类和StringBuilder类的父类,如果属于AbstractStringBuilder类型,则判断是否属于StringBuffer类型,如果是则用synchronized加锁调用nonSyncContentEquals方法,如果属于StringBuilder类则不加锁调用nonSyncContentEquals方法。如果参数属于String类型,则调用equals方法,如果属于一般的CharSequence类型,则判断字符串的长度与比较的字符序列的长度是否相等,如果不相等返回false,否则继续遍历比较两个比较对象底层char数组的每个位置的字符是否相等。最后再来分析下nonSyncContentEquals方法,nonSyncContentEquals方法如下:

private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
        char v1[] = value;
        char v2[] = sb.getValue();
        int n = v1.length;
        if (n != sb.length()) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (v1[i] != v2[i]) {
                return false;
            }
        }
        return true;
}

nonSyncContentEquals方法就是判断两个比较对象底层的char数组之间的相同位置的字符是否相等。首先判断比较序列之间的长度是否相等,然后遍历两个对象底层数组相同位置的字符是否相等。

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;
}

compareTo方法比较两个字符串的大小,首先获取两个字符串的长度,然后获得两个字符串的长度中较小的长度lim,遍历比较两个字符串小于lim范围里字符的大小,当两个字符串相同位置的字符不相等时,返回两个字符相减的结果,如果较小长度lim范围里,两个字符串的字符都相等,那么比较两个字符串的长度大小。

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.
        if ((toffset < 0) || (toffset > value.length - pc)) {
            return false;
        }
        while (--pc >= 0) {
            if (ta[to++] != pa[po++]) {
                return false;
            }
        }
        return true;
 }
//指定toffset参数为0,说明字符串是否包含指定的前缀prefix,索引从0开始。
public boolean startsWith(String prefix) {
        return startsWith(prefix, 0);
}
//从value.length - suffix.value.length索引开始的子字符串是否包含指定的后缀suffix
public boolean endsWith(String suffix) {
        return startsWith(suffix, value.length - suffix.value.length);
}

startsWith方法是从指定索引开始的字符串的子字符串是否包含指定的前缀。首先进行边界的判断,参数toffset是否小于0,以及toffset是否大于字符串的长度减去指定前缀的长度,如果toffset小于0或者toffset大于字符串的长度减去指定前缀的长度,则返回false,否则,遍历指定前缀与指定索引开始的字符串的子字符串的字符是否都相等,否则返回false。如果都不满足上述的条件,返回true。

startsWith(String prefix)方法和endsWith(String suffix) 都是在startsWith(String prefix, int toffset)方法的基础上来的,只是toffset的值不一样,当toffset等于0时,就变成了startsWith(String prefix)方法;当toffset等于value.length - suffix.value.length时,就变成了endsWith(String suffix)方法。

public int indexOf(int ch, int fromIndex) {
        //边界判断
        final int max = value.length;
        if (fromIndex < 0) {
            fromIndex = 0;
        } else if (fromIndex >= max) {
            // Note: fromIndex might be near -1>>>1.
            return -1;
        }
		//当ch<65535,一个字节8位,两个字节16位,2的16次方等于65535
        if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
            // handle most cases here (ch is a BMP code point or a
            // negative value (invalid code point))
            final char[] value = this.value;
            for (int i = fromIndex; i < max; i++) {
                if (value[i] == ch) {
                    return i;
                }
            }
            return -1;
        } else {
            return indexOfSupplementary(ch, fromIndex);
        }
}

在讲解indexOf(int ch, int fromIndex) 方法时,先了解下Unicode 编码,Unicode 编码是为了解决能够使计算机实现跨语言、跨平台的文本转换及处理。可以容纳世界上所有文字和符号的字符编码方案。本来想讲解下Unicode ,但是发现网上已经通俗易懂讲解Unicode 编码的文章,所以就不展开来讲了,感兴趣的可以参考如下博客:

http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

indexOf(int ch, int fromIndex) 方法首先进行边界判断,如果输入的参数字符ch小于65535,就直接遍历String类型的char数组value,参数小于65535,说明该字符由两个字节组成。当value数组中的字符与参数ch相等,就返回该位置的索引。如果输入的参数字符大于等于65535,就调用indexOfSupplementary(int ch, int fromIndex) 方法,该方法如下:

private int indexOfSupplementary(int ch, int fromIndex) {
    //判断是否是合法的Unicode 对应的码点
     if (Character.isValidCodePoint(ch)) {
          final char[] value = this.value;
          final char hi = Character.highSurrogate(ch);
          final char lo = Character.lowSurrogate(ch);
          final int max = value.length - 1;
          for (int i = fromIndex; i < max; i++) {
              if (value[i] == hi && value[i + 1] == lo) {
                  return i;
              }
           }
       }
       return -1;
}

当输入参数大于等于两个字节,即大于等于65535时,调用indexOfSupplementary(int ch, int fromIndex) 方法,当输入的参数字符ch是合法的码点,获取输入参数ch的大端字符和小端字符,比较ch的大端字符和小端字符与value数组第i个位置和第i+1个位置的值是否相等,相等就返回索引i。如果没有找到相等的值,就返回-1。

public int lastIndexOf(int ch, int fromIndex) {
        if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
            // handle most cases here (ch is a BMP code point or a
            // negative value (invalid code point))
            final char[] value = this.value;
            int i = Math.min(fromIndex, value.length - 1);
            for (; i >= 0; i--) {
                if (value[i] == ch) {
                    return i;
                }
            }
            return -1;
        } else {
            return lastIndexOfSupplementary(ch, fromIndex);
        }
}

private int lastIndexOfSupplementary(int ch, int fromIndex) {
        if (Character.isValidCodePoint(ch)) {
            final char[] value = this.value;
            char hi = Character.highSurrogate(ch);
            char lo = Character.lowSurrogate(ch);
            int i = Math.min(fromIndex, value.length - 2);
            for (; i >= 0; i--) {
                if (value[i] == hi && value[i + 1] == lo) {
                    return i;
                }
            }
        }
        return -1;
}

lastIndexOf(int ch, int fromIndex)方法与indexOf(int ch, int fromIndex) 方法逻辑基本一样,只是两者在遍历数组value的顺序不一样,lastIndexOf(int ch, int fromIndex)是从数组后面往前面遍历,indexOf(int ch, int fromIndex) 是从数组前面往后面遍历。

indexOf和lastIndexOf方法都很多其他参数的重载方法,都是在indexOf(int ch, int fromIndex) 方法和lastIndexOf(int ch, int fromIndex)方法的基础上拓展而来的,只要理解这两个方法,其他重载的方法都比较简单,这里就不分析了。

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

substring(int beginIndex)方法返回从beginIndex位置开始的子字符串,首先判断beginIndex是否合法,当beginIndex小于0或者子字符串的小于0时(value.length - beginIndex),抛出StringIndexOutOfBoundsException异常。当beginIndex等于0时,直接返回this,返回字符串是原来的字符串,否则创建新的String对象返回。

public String substring(int beginIndex, int endIndex) {
        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);
}

substring(int beginIndex, int endIndex) 方法是返回从beginIndex开始到endIndex位置结束的子字符串。substring(int beginIndex, int endIndex) 首先判断beginIndex和endIndex的合法性,当beginIndex和endIndex不合法时,抛出StringIndexOutOfBoundsException异常。beginIndex等于0并且endIndex 等于 value.length,返回this(原字符串),否则重新创建一个String对象返回。

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);
}

concat(String str)方法的作用是拼接两个字符串,当拼接的字符串str的长度等于0,直接返回原字符串。否则通过 Arrays.copyOf将value数组复制到长度为len + otherLen的字符数组buf中,然后将字符串复制到buf数组中,最后用buf数组创建新的字符串返回。

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */
			//先找到字符串中与oldChar相等的值的位置i
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            //如果找到字符串中与oldChar相等的值的位置i
            if (i < len) {
                char buf[] = new char[len];
                //将小于i位置的字符保存在buf中
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                //将所有等于oldChar值的替换为newChar,否则等于原来的字符串
                while (i < len) {
                    char c = val[i];
                    //如果i位置的
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                //返回新创建的String对象
                return new String(buf, true);
            }
        }
        return this;
}

replace(char oldChar, char newChar)方法的作用是将字符串中老字符oldChar替换为新字符newChar,如果oldChar等于newChar,直接返回原来的字符串,否则先找到字符串中与oldChar相等的第一个字符的位置i。如果找到字符串中与oldChar相等的第一个字符的位置i,将用两步来替换旧的值,第一步将小于i位置的字符保存在buf,第二部将所有等于oldChar值的替换为newChar,如果不等于oldChar将原来的字符串保存在buf,最后通过buf创建新的String对象。

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;
}

trim() 方法是去除字符串两边的空字符‘ ’, trim()方法从字符串的前面和后面分别遍历字符,寻找不等于空字符‘ ’的位置,如果原来字符串前面和后面存在空字符串' ',返回去除空字符串' '的子字符串,否则,返回原来的字符串。

public char[] toCharArray() {
       // Cannot use Arrays.copyOf because of class initialization order issues
      char result[] = new char[value.length];
      System.arraycopy(value, 0, result, 0, value.length);
      return result;
}

toCharArray()方法返回字符串的char数组,通过本地方法 System.arraycopy复制字符串的字符到char数组中。该方法中有一段注释说明不能使用Arrays.copyOf复制字符到char数组中,是因为类初始化的顺序问题。这注释也没有很清楚解释为什么toCharArray()中不能用Arrays.copyOf复制字符。经过查找资料,找到一段清楚的解释:

引用:https://my.oschina.net/u/3268478/blog/3011267

虽然String 和Arrays 都属于rt.jar中的类,但是BootstrapClassloader 在加载这两个类的顺序是不同的。所以当String.class被加载进内存的时候,Arrays此时没有被加载,所以直接使用肯定会抛异常。而System.arrayCopy是使用native代码,则不会有这个问题。


public native String intern();

intern()是本地方法,当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。对于两个字符串s和t,如果s.intern() == t.intern()为true,则s.equals(t)为true。intern()方法在JVM的函数为Java_java_lang_String_intern:

JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
    return JVM_InternString(env, this);
}

Java_java_lang_String_intern函数返回调用JVM_InternString函数的结果。JVM_InternString函数的方法为:

JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  JVMWrapper("JVM_InternString");
  JvmtiVMObjectAllocEventCollector oam;
 //如果java中的字符串等于null,直接返回null
  if (str == NULL) return NULL;
//将java的字符串jstring转为c++中的普通对象oop
  oop string = JNIHandles::resolve_non_null(str);
//从字符常量池StringTable中查找字符串,
  oop result = StringTable::intern(string, CHECK_NULL);
//将从字符常量池StringTable中查找字符串的转换成java的字符串
  return (jstring) JNIHandles::make_local(env, result);
JVM_END

JVM_InternString函数从StringTable常量池中寻找字符串,StringTable相等于java中hashTable表,如果不存在这个字符串,那么就将这个字符串的引用保存在StringTable常量池中,如果存在这个字符串,就返回这个字符串的引用。StringTable常量池中的字符串越来越多的时候,查找效率会越来越低,所有也不能大量的使用intern(),否则导致性能下降。

相关推荐

如何在HTML中使用JavaScript:从基础到高级的全面指南!

“这里是云端源想IT,帮你...

推荐9个Github上热门的CSS开源框架

大家好,我是Echa。...

前端基础知识之“CSS是什么?”_前端css js

...

硬核!知网首篇被引过万的论文讲了啥?作者什么来头?

整理|袁小华近日,知网首篇被引量破万的中文论文及其作者备受关注。知网中心网站数据显示,截至2021年7月23日,由华南师范大学教授温忠麟等人发表在《心理学报》2004年05期上的学术论文“中介效应检验...

为什么我推荐使用JSX开发Vue3_为什么用vue不用jquery

在很长的一段时间中,Vue官方都以简单上手作为其推广的重点。这确实给Vue带来了非常大的用户量,尤其是最追求需求开发效率,往往不那么在意工程代码质量的国内中小企业中,Vue占据的份额极速增长...

【干货】一文详解html和css,前端开发需要哪些技术?
【干货】一文详解html和css,前端开发需要哪些技术?

网站开发简介...

2025-02-20 18:34 yuyutoo

分享几个css实用技巧_cssli

本篇将介绍几个css小技巧,目录如下:自定义引用标签的符号重置所有标签样式...

如何在浏览器中运行 .NET_怎么用浏览器运行代码

概述:...

前端-干货分享:更牛逼的CSS管理方法-层(CSS Layers)

使用CSS最困难的部分之一是处理CSS的权重值,它可以决定到底哪条规则会最终被应用,尤其是如果你想在Bootstrap这样的框架中覆盖其已有样式,更加显得麻烦。不过随着CSS层的引入,这一...

HTML 基础标签库_html标签基本结构
HTML 基础标签库_html标签基本结构

HTML标题HTML标题(Heading)是通过-...

2025-02-20 18:34 yuyutoo

前端css面试20道常见考题_高级前端css面试题

1.请解释一下CSS3的flexbox(弹性盒布局模型),以及适用场景?display:flex;在父元素设置,子元素受弹性盒影响,默认排成一行,如果超出一行,按比例压缩flex:1;子元素设置...

vue引入外部js文件并使用_vue3 引入外部js

要在Vue中引入外部的JavaScript文件,可以使用以下几种方法:1.使用``标签引入外部的JavaScript文件。在Vue的HTML模板中,可以直接使用``标签来引入外部的JavaScrip...

网页设计得懂css的规范_html+css网页设计

在初级的前端工作人员,刚入职的时候,可能在学习前端技术,写代码不是否那么的规范,而在工作中,命名的规范的尤为重要,它直接与你的代码质量挂钩。网上也受很多,但比较杂乱,在加上每年的命名都会发生一变化。...

Google在Chrome中引入HTML 5.1标记

虽然负责制定Web标准的WorldWideWebConsortium(W3C)尚未宣布HTML5正式推荐规格,而Google已经迁移到了HTML5.1。即将发布的Chrome38将引入H...

HTML DOM 引用( ) 对象_html中如何引用js

引用对象引用对象定义了一个同内联元素的HTML引用。标签定义短的引用。元素经常在引用的内容周围添加引号。HTML文档中的每一个标签,都会创建一个引用对象。...

取消回复欢迎 发表评论: