【Java】JDK源码分析——String

一.概述

String类是Java中的用于创建和处理字符串的类。String类对象一旦创建,不能修改,即不可变。
String.java中的相关代码:

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

1.String类被final关键字修饰,因此不可以被继承。
2.实现了java.io.Serializable接口,可以进行序列化。
3.实现Comparable接口,可以进行字符串的比较。
4.实现了CharSequence接口,可以将字符串当成一个一个的字符处理。

二.源码解析

1.重要的全局变量

String.java中的相关代码:

    private final char value[]; // 用于保存字符串

    private int hash; // 字符串的哈希值,默认为0

	private static final long serialVersionUID = -6849794470754667710L; // 序列化UID

    // 用于比较两个字符串谁包含的范围更大,比较时忽略字符大小写
	public static final Comparator<String> CASE_INSENSITIVE_ORDER
    	                                     = new CaseInsensitiveComparator();

2.常用的构造方法

1)无参数

String.java中的相关代码:

	public String() {
        this.value = "".value;
	}

无参数时默认的字符串为””。

2)参数为String

String.java中的相关代码:

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
	}

保存参数的值和参数的哈希值的引用,因为original是不可变的,所以不需要再创建一个和original一模一样的新对象。

3)参数为char[]

String.java中的相关代码:

    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
	}

根据数组的内容创建一个新对象,保存新创建的数组对象,因为字符数组的内容是可变的,所以需要创建新的对象。

4)参数为byte[]

String.java中的相关代码:

	public String(byte bytes[]) {
        this(bytes, 0, bytes.length);
	}

调用了重载的构造方法,从0开始,长度为字节数组的长度。

String.java中的相关代码:

	public String(byte bytes[], int offset, int length) {
    	// 对边界进行检查,详解在a)处
        checkBounds(bytes, offset, length);
        // 对字节数组进行解码,并保存结果
        this.value = StringCoding.decode(bytes, offset, length);
	}

a)checkBounds方法

	private static void checkBounds(byte[] bytes, int offset, int length) {
    	// 长度必须大于零
        if (length < 0)
            throw new StringIndexOutOfBoundsException(length);
        // 起始位置必须大于零
        if (offset < 0)
            throw new StringIndexOutOfBoundsException(offset);
        // 指定的长度必须小于数组的总长度
        // 采用减法的方式,可以防止当offset+length大于最大值-1>>>1而报错
        if (offset > bytes.length - length)
            throw new StringIndexOutOfBoundsException(offset + length);
	}

5)参数为StringBuffer

String.java中的相关代码:

	public String(StringBuffer buffer) {
    	// 同步锁,锁对象为buffer
        synchronized(buffer) {
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        }
	}

根据buffer中保存的字符串的值创建一个新对象并保存新对象,因为StringBuffer中字符串长度可变。因为StringBuffer需要保证线程安全,所以加锁。

6)参数为StringBuilder

String.java中的相关代码:

    public String(StringBuilder builder) {
        this.value = Arrays.copyOf(builder.getValue(), builder.length());
	}

根据builder中保存的字符串的值创建一个新对象并保存新对象,因为StringBuilder中字符串长度可变。因为StringBuilder不需要保证线程安全,所以不加锁。

3. length方法

获取字符串长度。
String.java中的相关代码:

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

返回字符数组的长度。

4.isEmpty方法

判断字符串是否为空。
String.java中的相关代码:

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

通过比较字符数组长度进行判断。

5. charAt方法

获取字符串中指定位置的字符。
String.java中的相关代码:

	public char charAt(int index) {
    	// 若指定的位置超过了数组的边界,则抛出异常
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        // 满足条件,返回字符数组中指定位置的字符
        return value[index];
	}

6.getBytes方法

指定字符集,获取字符串对应的字节数组。
String.java中的相关代码:

	public byte[] getBytes(Charset charset) {
    	// 若指定的字符集为空,则抛出异常
        if (charset == null) throw new NullPointerException();
        // 满足条件,根据字符集将字符数组编码成字节数组并返回
        return StringCoding.encode(charset, value, 0, value.length);
	}

7.equels方法

比较两个String对象是否相等。
String.java中的相关代码:

	public boolean equals(Object anObject) {
    	// 若两个对象为同一个对象,则返回true
        if (this == anObject) {
            return true;
        }
        // 若比较的对象类型为String
        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) {
                    // 若发现有不相等的字符,则返回false
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                // 若全部相等,则返回true;
                return true;
            }
        }
        // 待比较的对象不是String类型,返回false
        return false;
	}

8. compareTo方法

比较两个字符串谁包含的字符范围更大
String.java中的相关代码:

	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++;
        }
        // 若两个字符串长度不相等,且最小长度内的按位比较,两者都相等
        // 则返回两个字符串的长度之差 eg: abcdef和abc。
        return len1 - len2;
	}

9. compareToIgnoreCase方法

在忽略字符大小写的情况下,比较两个字符串谁包含的字符范围更大
String.java中的相关代码:

    public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
	}

调用了全局变量CASE_INSENSITIVE_ORDER的compare方法。
CASE_INSENSITIVE_ORDER是一个CaseInsensitiveComparator类型的对象。
CaseInsensitiveComparator是String的静态内部类。
String.java中的相关代码:

	private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // 用于序列化
        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++) {
                // 获取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;
                        }
                    }
                }
            }
            // 若两个字符串长度不相等,且最小长度内的按位比较,两者都相等
            // 则返回两个字符串的长度之差 eg: abcdef和abc。
            return n1 - n2;
        }

        // 返回全局变量,用于替换反序列化的对象
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
	}

为什么两个字符大写形式比较不相等,还要比较两个字符的小写形式?
官方说法是因为格鲁吉亚字母的存储规则和英文字母不同。

10. regionMatches方法

判断两个从不同起始位置开始的字符串,在一定的长度内的值是否相等
String.java中的相关代码:

	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;
        // 若不满足边界条件,则返回false
        // 采用减法形式比较,因为相加后的结果可能超过最大值-1>>>1,导致程序崩溃
        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;
        }
        // 最后返回true
        return true;
	}

11. equalsIgnoreCase方法

忽略大小写的情况下比较两个字符串是否相等
String.java中的相关代码:

    public boolean equalsIgnoreCase(String anotherString) {
        return (this == anotherString) ? true
                : (anotherString != null)
                && (anotherString.value.length == value.length)
                && regionMatches(true, 0, anotherString, 0, value.length);
	}

若两个字符串为同一个对象,则返回true。
否则需要同时满足三个条件:1)另一个字符串不为空。2)两个字符串长度相等。3)忽略大小写时,两个字符串对应位置的字符相等。

12. startsWith方法

判断一个字符串从头开始的连续长度内的字符是否和指定字符串的内容相等。
String.java中的相关代码:

    public boolean startsWith(String prefix) {
        return startsWith(prefix, 0);
	}

调用了重载的方法。
判断一个字符串从指定位置开始的连续长度内的字符是否和指定的字符串中的字符匹配。
String.java中的相关代码:

	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;
        // 若不满足边界条件,则返回false
        if ((toffset < 0) || (toffset > value.length - pc)) {
            return false;
        }
        // 循环比较
        while (--pc >= 0) {
            // 若不相等,则返回false
            if (ta[to++] != pa[po++]) {
                return false;
            }
        }
        // 全部相等,返回true
        return true;
	}

13. endsWith方法

判断一个字符串从末尾向前的连续长度内的字符是否和指定字符串的内容相等。
String.java中的相关代码:

	public boolean endsWith(String suffix) {
    	// 用两个字符串的长度做差,得出起始位置
        return startsWith(suffix, value.length - suffix.value.length);
	}

14. hashCode方法

求一个字符串的哈希值。
String.java中的相关代码:

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

计算公式为s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1],n为字符串的长度。

15. indexOf方法

获取字符串中指定的字符首次出现的位置。
String.java中的相关代码:

    public int indexOf(int ch) {
        return indexOf(ch, 0);
	}

调用了重载方法。
获取字符串中指定起始位置开始,指定的字符首次出现的位置。
String.java中的相关代码:

	public int indexOf(int ch, int fromIndex) {
    // 获取字符串长度
        final int max = value.length;
        // 若起始位置小于0
        if (fromIndex < 0) {
            // 则默认起始位置为0
            fromIndex = 0;
        // 或起始位置超过字符串的长度
        } else if (fromIndex >= max) {
            // 返回-1,说明指定的字符在字符串中没有出现
            return -1;
        }

        // 若字符ch为BMP字符,即占用空间为两个字节
        // 详解在1)处
        if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
            // 字符串的字符数组
            final char[] value = this.value;
            // 从起始位置开始循环
            for (int i = fromIndex; i < max; i++) {
                // 若有相等的字符出现,则返回出现的位置
                if (value[i] == ch) {
                    return i;
                }
            }
            // // 返回-1,说明指定的字符在字符串中没有出现
            return -1;
        } else { //若ch为增补字符集中的字符。
            // 详解在2)处
            return indexOfSupplementary(ch, fromIndex);
        }
	}

1)UTF-16编码方式下,为每16bit表示一个字符,一共可以表示65536种字符,这种占用两个字节的字符称为BMP字符。但是Unicode编码方式为了表示更多的字符,同时还要用16bit表示,于是引入了Surrogates,即从UTF-16的65536种字符中选择2048种,让这些字符两个为一组来表示超出65536的字符。进一步将这2048种字符分成High Surrogates和Low Surrogates两部分,每部分1024种字符,一共可以表示1048576种字符。
      UTF-16编码下的High Surrogates和Low Surrogates通过一定的规则计算,可以得到16bit的Unicode编码下的字符。

2)indexOfSupplementary方法
String.java中的相关代码:

    private int indexOfSupplementary(int ch, int fromIndex) {
        if (Character.isValidCodePoint(ch)) {
            final char[] value = this.value;
            // 获取High Surrogate
            final char hi = Character.highSurrogate(ch);
            // 获取Low Surrogates
            final char lo = Character.lowSurrogate(ch); 
            // 获取字符串长度
            final int max = value.length - 1;
            // 循环比较
            for (int i = fromIndex; i < max; i++) {
                // 若High Surrogates和Low Surrogates都相等,则返回对应位置
                if (value[i] == hi && value[i + 1] == lo) {
                    return i;
                }
            }
        }
        // 返回-1,说明指定的字符在字符串中没有出现
        return -1;
	}

16. substring方法

从指定的起始位置开始,对字符串进行截取。
String.java中的相关代码:

	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,说明截取的字符串为全部,直接返回
        // 若起始位置大于0,创建新的字符串,返回
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
	}

调用String的构造方法,将字符数组中指定位置和长度的字符变成字符串
String.java中的相关代码:

	public String(char value[], int offset, int count) {
    	// 若起始位置小于0
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        // 若指定长度小于等于0
        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);
        }
        // 对字符数组指定范围的字符进行复制,并保存
        this.value = Arrays.copyOfRange(value, offset, offset+count);
	}

17. replace方法

将字符串中的某一种字符替换成另一种字符
String.java中的相关代码:

	public String replace(char oldChar, char newChar) {
    	// 若要替换的字符和替换后的字符不是相同的字符
        if (oldChar != newChar) {
            // 获取字符串长度
            int len = value.length;
            // 用于记录首个需要替换字符的位置
            int i = -1;
            // 获取字符串对应的字符数组
            char[] val = value; 

            // 遍历查找字符串中是否有需要替换的字符
            // 同时记录首个需要替换的字符的位置
            while (++i < len) {
                // 若i位置的字符为需要替换的字符
                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];
                    // 若i位置字符为待替换的字符,则替换
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                // 根据新创建的字符数组的内容,创建新的字符串,返回
                return new String(buf, true);
            }
        }
        // 若要替换的字符和替换后的字符一样,则不需要替换,直接返回
        return this;
	}

18.contains方法

判断一个字符串是否含有某个字符
String.java中的相关代码:

	public boolean contains(CharSequence s) {
    	// 若字符串中字符s第一次出现的位置大于-1,则包含字符s
        return indexOf(s.toString()) > -1;
	}

19.split方法

根据给出指定的字符或正则表达式规则,将字符串拆分成数组
String.java中的相关代码:

    public String[] split(String regex) {
        return split(regex, 0);
	}

调用了重载方法。
String.java中的相关代码:

    public String[] split(String regex, int limit) {
        // 拆分的界限,即以ch为界限进行拆分
        char ch = 0;
        // 对ch赋值,同时若满足以下条件
        // 详解在1)处
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              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 ||
             ch > Character.MAX_LOW_SURROGATE))
        {
            // 查找的起始位置
            int off = 0;
            // ch字符出现的位置
            int next = 0;
            // 表示对拆分结果的数量是否有限制
            boolean limited = limit > 0;
            // 用于保存拆分后的结果
            ArrayList<String> list = new ArrayList<>();
            // 循环,从off位置开始,查找字符ch首次出现的位置,并赋值给next
            while ((next = indexOf(ch, off)) != -1) {
                // 如果对结果数量没有限制
                // 或者有限制的情况下,已经拆分出来的结果数量小于限制数量减1
                // 减1是因为如果对结果数量有限制,
				// 最后一次需要把剩余的字符全部添加到list,不需拆分,该过程在else处
                if (!limited || list.size() < limit - 1) {
                    // 对字符串截取,并保存
                    list.add(substring(off, next));
                    // 设置下一次查找的起始位置
                    off = next + 1;
                } else {    
					// 若对结果数量有限制,同时已经拆分出来的结果数量等于限制数量减1
					// 即本次为有限制情况下的最后一次
					// 对字符串截取,从off开始截取到最后,保存
                    list.add(substring(off, value.length));
                    // 设置下一次查找的起始位置
                    off = value.length;
                    // 跳出循环
                    break;
                }
            }
            // 若起始位置为0,说明没有进行上面的循环
            // 说明字符串中不包含用于划分界限字符
            if (off == 0)
                // 返回值为自身的新对象
                return new String[]{this};

            // 若没有限制结果数量,或拆分出的字符串的数量小于限制要求
            // 这里用于添加在没有限制的情况下的最后一个拆分出的字符串,
			// 因为上面循环在最后一次时,next=-1跳出循环,没有添加到list中
			// 同时,若存在限制,且限制的数量大于等于实际可以拆分出的结果数量
			// 也需要在这里添加最后一个拆分出的字符串
            if (!limited || list.size() < limit)
                // 截取,添加
                list.add(substring(off, value.length));

            // 获取拆分结果的数量
            int resultSize = list.size();
            // 若limit为0,说明没有限制
            if (limit == 0) {
                // 从后向前剔除末尾长度为0的字符串
                // 直到末尾出现第一个长度不为0的字符串,跳出循环
                // 因为当界限字符连续出现在末尾,会产生长度为0的字符串
                // 这些字符串没有意义,需要剔除
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                    // 减一,记录有效结果的数量
                    resultSize--;
                }
            }
            // 创建字符串数组
            String[] result = new String[resultSize];
            // 根据有效结果的数量对list截取,复制结果到字符串数组,返回
            return list.subList(0, resultSize).toArray(result);
        }
        // 若regex为正则表达式,则交给Pattern类处理
        return Pattern.compile(regex).split(this, limit);
	}

1)满足条件
      若regex长度为1,则对ch赋值,同时ch不能为 .、$、|、(、)、[、{、^、?、*、+、\这些字符,因为这些字符是正则表达式中的内容。\为\的转义字符。
      若regex长度为2,则regex的第一个字符必须为\,ch的值为regex第二个字符。同时ch只能为0-9、a-z、A-Z之间的字符,代表regex转义字符。
最后,无论regex长度为1还是2,ch必须为UTF-16字符集中除了用于构成Unicode中扩展字符集的其他字符。
      regex其它情况当作正则表达式处理。

20. trim方法

去除字符串前面和后面的空格,字符串中间的空格不做处理。
String.java中的相关代码:

	public String trim() {
    	// 获取字符串长度
        int len = value.length;
        // 设置起始位置
        int st = 0;
        // 获取字符串的字符数组
        char[] val = value; 

        // ASCII码表小于空格的符号都是一些分隔符,控制符等。
        // 当小于等于空格,且长度满足要求
        while ((st < len) && (val[st] <= ' ')) {
            // 起始位置自增,跳过这些符号
            st++;
        }
        // 同理
        while ((st < len) && (val[len - 1] <= ' ')) {
            // 从后向前,跳过这些符号
            len--;
        }
        // 若起始位置大于0或终止位置小于字符串长度
        // 则截取并返回,否则返回自身
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
	}

21. toCharArray方法

将字符串转换为字符数组。
String.java中的相关代码:

    public char[] toCharArray() {
        // 创建新字节数组
        char result[] = new char[value.length];
        // 复制字符串的字节数据到新数组
        System.arraycopy(value, 0, result, 0, value.length);
        // 返回新数组
        return result;
	}

这里复制数组内容不使用Arrays的copyOf方法,因为String类加载完成时,Arrays类还没有被加载,这时候调用,会抛出异常。

22. valueOf方法

1)参数为Object

将Object类型的对象转化为String类型
String.java中的相关代码:

	public static String valueOf(Object obj) {
    	// 若不为空,调用toString方法并返回,否则返回null
        return (obj == null) ? "null" : obj.toString();
	}

2)参数为char[]

将char[]类型的对象转化为String类型
String.java中的相关代码:

	public static String valueOf(char data[]) {
    	// 直接创建新String对象并返回
        return new String(data);
    }

3)参数为boolean

将boolean类型的对象转化为String类型
String.java中的相关代码:

	public static String valueOf(boolean b) {
    	// 若为true,则返回“true”,否则返回“false”
        return b ? "true" : "false";
    }

4)参数为int

将int类型的对象转化为String类型
String.java中的相关代码:

	public static String valueOf(int i) {
    	// 调用int的包装类Integer的toString方法
        return Integer.toString(i);
	}

参数为float、double、long时同理,都是通过调用其包装类的静态方法toString实现。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章