【java】Object类详细分析

API中对Object的介绍

  • Class {@code Object} is the root of the class hierarchy.(类Object是类层次结构的根类)
  • Every class has {@code Object} as a superclass. All objects,(每一个类都要将Object类作为父类)
  • including arrays, implement the methods of this class.(所有对象,包括数组在内,都要实现这个类中的方法)

Object类中包含的成员变量和方法简介

Object类中不包含成员变量,只包含13个方法

这里写图片描述

naive关键字

因为java中有很多地方都使用naive关键字,因此在这里简单总结一下,总结的内容来自:http://blog.csdn.net/youjianbo_han_87/article/details/2586375

  • native关键字是与C++联合开发的时候用的,java自己开发不用!
  • 使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。这个函数的实现提在DLL中,JDK的源代码中并不包含,所以看不到被这个关键字修饰的函数的源代码。对于不同的平台它们是不同的。这也是java的底层机制,实际上java就是在不同平台上调用不同的native方法实现对操作系统的访问。
  • native是用做java和其他语言(如C)进行协作时用的,也就是native后的函数的实现不是用java写的。
  • native的意思就是通知操作系统,这个函数你必须给我实现,因为我要用。所以native关键字的函数就是操作系统实现的,java只能调用。
  • java是跨平台的语言,既然是跨平台,所付出的代价就是牺牲一些对底层的控制,而java要实现对底层的控制,就需要一些其他语言的帮助,这个就是native的作用了

Object类中的方法

首先看一下java.lang.Object中的源码:

public class Object {

    private static native void registerNatives();
    
    static {
        registerNatives();
    }
    
    public final native Class<?> getClass();
    
    public native int hashCode();
    
    public boolean equals(Object obj) {
        return (this == obj);
    }  
    protected native Object clone() throws CloneNotSupportedException;
    
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }  
    
    public final native void notify();
    
    public final native void notifyAll();
    
    public final native void wait(long timeout) throws InterruptedException;
    
    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }
    
    public final void wait() throws InterruptedException {
        wait(0);
    }
    
    protected void finalize() throws Throwable { }

类构造器

在类定义过程中,对于未定义构造函数的类,默认会有一个无参的构造函数,Object作为所有类的基类,在源码中,并未给出显示的构造函数,但是实际上,此构造函数是存在的。是JVM自动生成的。


1. private static native void registerNatives()方法

主要作用是将C/C++中的方法映射到java中的native方法,实现方法命名的解耦。在Object类中,在该方法的声明后有一段静态代码块,用来调用该方法:

private static native void registerNatives();  
static {  
     registerNatives();  
}  

2. public final nativeClass<?> getClass()方法

该方法也是native方法,返回的是该Object对象的类对象/运行时类对象。

类对象: 在java中,类是对具有一组相同特征或者行为的实例的抽象并进行描述,对象是该类所描述的特征或者行为的具体实例。作为概念层次的类,其本身也具有某些共同的属性,如都具有类型、类加载器、包、超类、属性、方法等。java中有专门定义一个类,Class,该类用于描述其他类所具有的这些特征。因此,从该角度来看,类本身也都是属于Class类的对象。 该部分涉及到反射的知识


3. public native int hashCode()方法

hashCode的API说明:

  1. Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
    在java应用程序执行期间,在对同一个对象多次调用hashCode方法时,必须返回相同的整数。前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  2. If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
    如果根据equals(Object)方法,两个对象是相等的,那么对这两个对象调用hashCode方法都必须生成相同的整数结果。
  3. It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
    如果根据equals(java.lang.Object)方法,两个对象不同等,那么对这两个对象中的任意一个对象调用hashCode方法时,不要求一定生成不同的整数结果
    就是说:两个对象不等,那么hashCode可以相等也可以不等;hashCode不等,两个对象一定不等

hashCode返回的值:一般是通过将该对象的内存地址转换成一个整数来实现。该方法不是java实现的,因此使用了native关键字。

hashCode的出现主要是用于增强哈希表的性能:

以集合类中,以Set为例,当新加一个对象时,需要判断现有集合中是否已经存在与此对象相等的对象,如果没有hashCode()方法,需要将Set进行一次遍历,并逐一用equals()方法判断两个对象是否相等,此种算法时间复杂度为o(n)。通过借助于hasCode方法,先计算出即将新加入对象的哈希码,然后根据哈希算法计算出此对象的位置,直接判断此位置上是否已有对象即可


4. public boolean equals(Object obj)方法

equals()方法的内部原理
public boolean equals(Object obj) {
    return (this == obj);
}  

这是Object类中提供的equals()方法的源码,通过该源码可以看出,equals内部是通过使用==运算符进行判断两个对象是否相等的。

equals()方法与==运算符

==运算符在比较基本数据类型和引用数据类型的时候含义是不同的:

  • 基本数据类型之间的比较,应用==运算符,比较的是他们的值
  • 引用数据类型用==运算符比较的时候,比较的是在内存中的存放地址,所以,除非是实例化的同一个对象比较后的结果才能是true,否则比较的结果是false**
  • String类等包装类,都对equals方法进行了重写,所以现在调用String类的该方法,比较的是内容是否相等。
  • 当一个类的equlas()方法被重写时,通常需要重写hashCode方法,以维护hashCode方法最开始的声明,即相等的两个对象必须拥有相等的哈希码<-这个理由在下面一节中会具体讲解

比较两个自定义类的时候,如果没有重写equals()方法,那么比较的就是内存地址是否相等。

public class EqualsTest {
	public static void main(String[] args){
		Bird b1 = new Bird(1);
		Bird b2 = new Bird(1);
		System.out.println("b1.equals(b2):" + b1.equals(b2));
	}
}

class Bird {
	private int size;

	public Bird(int size) {
		super();
		this.size = size;
	}
}

程序运行结果:

b1.equals(b2)false

但是通常情况下,我们希望equals()方法比较的是两个对象的内容是否相等。
上述程序,因为Bird类没有重写equals()方法,因此会调用父类对应的方法,而Object类的 equals()方法比较的是内存地址。b1和b2的值都是new出来的,因此,内存地址一定不同,返回结果一定是false;

现在我们修改一下Bird,让它重写equals方法,使得调用该方法的时候比较的是对象中存储的内容。

	@Override  
	public boolean equals(Object obj) {
		if(obj instanceof Bird) {
			Bird b = (Bird) obj;
			return size == b.size;
		}
		return false;
	}

重写了equals方法后,再运行程序,输出的结果为:

b1.equals(b2)true

这个时候比较的是两个对象存的具体的值是否相等。
【注意】:这个equals方法在比较同一个对象的时候,这么改写是没有问题的,但是当比较是继承关系的子类和父类的时候,使用instanceof会出现问题。

【总结】:想要通过equals()方法比较两个对象所存的内容是否相等的时,一定要重写equals()方法

equals方法的重写规则
  • 自反性:对于任何非null的引用值,x.equals(x)应该返回true
  • 对称性:对于任何非null的引用值,当x.equals(y)返回true时,y.equals(x)一定返回true
  • 传递性:对于任何非null的引用值,当x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)一定返回true
  • 一致性:对于任何非null的引用值x与y,假设对象上equals比较中的信息没有被修改,则多次调用x.equals(y)始终返回true或者始终返回false
  • 对于任何非空引用值x,x.equal(null)应返回false

对于同一个类的不同对象间的比较时,这些规则都会满足,但是当比较不同类的不同对象时,一定要注意保证上述规则都要满足。下面看一下父类和子类之间使用equals方法出现的问题。

子类:

class Starling extends Bird{
	private int bigSize;

	public Starling(int size, int bigSize) {
		super(size);
		this.bigSize = bigSize;
	}
	
	public boolean equals (Object obj) {
		if(obj instanceof Starling){
			Starling s = (Starling) obj;
			return bigSize == s.bigSize && super.equals(s);
		}
		return false;
	}
}

主函数

		Bird b1 = new Bird(1);
		Bird b2 = new Bird(1);
		System.out.println("b1.equals(b2):" + b1.equals(b2));
		
		Starling s = new Starling(1,20); //第一个参数是父类的
		System.out.println("s.equals(b1):" +s.equals(b1));
		System.out.println("b1.equals(s):" +b1.equals(s));

程序执行结果:

b1.equals(b2)true
s.equals(b1)false
b1.equals(s)true

这样的执行结果违反了对称性原则,即两个对象互相调用equals方法时,执行的结果应该是一样的。问题出在哪里呢,实际上就是instanceof操作符搞得鬼,instanceof操作符,当(子类 instanceof 父类)时,返回的结果是true。所以这里面需要修改一个子类的equals方法。即在if(obj instanceof Starling)不成立时,不直接返回false,而是调用父类的equals方法。

	public boolean equals (Object obj) {
		if(obj instanceof Starling){
			Starling s = (Starling) obj;
			return bigSize == s.bigSize && super.equals(s);
		}
		return super.equals(obj);
	}

程序执行结果:

b1.equals(b2)true
s.equals(b1)true
b1.equals(s)true

这样就符合了规则中的自反性。
但是不幸的是,程序没有传递性

当测试数据如下时,就会发现这样写equals方法没有传递性:

		Bird b1 = new Bird(1);
		Bird b2 = new Bird(1);
		System.out.println("b1.equals(b2):" + b1.equals(b2));
		
		Starling s = new Starling(1,20); //第一个参数是父类的
		Starling s1 = new Starling(1,22); //第一个参数是父类的
		
		System.out.println("s.equals(b1):" +s.equals(b1));
		System.out.println("b1.equals(s1):" +b1.equals(s1));
		System.out.println("s.equals(s1):" +s.equals(s1));

程序的执行结果:

b1.equals(b2)true
s.equals(b1)true
b1.equals(s1)true
s.equals(s1)false

这样的结果是有问题的,因为s与b1是相等的,b1与s2是相等的,那结果应该是s与s1是相等的,但是实际上因为s与s1的自己的成员变量不同,所以返回的记过为false;
出现这种现象的根本原因:
父类与子类进行混合比较的时候,子类中声明了新的成员变量,并且在子类equals方法中使用了新增的成员变量作为判断是否相等的条件,在这种情况下,就会出现传递性失效的问题。
那么如何解决这个问题呢。目前还没有直接的解决办法,但是间接的解决办法还是有的。那就是使用组合代替继承,但是要注意组合的方式并没有真正意义上的解决问题(只是让它们见的比较都返回了false,从而不违背传递性)

class Starling {
	private int bigSize;
	private Bird b ;
	public Starling(int size, int bigSize) {
		b = new Bird(size);
		this.bigSize = bigSize;
	}
	
	public boolean equals (Object obj) {
		if(obj instanceof Starling){
			Starling s = (Starling) obj;
			return b.equals(s.b) && bigSize == s.bigSize;
		}
		return super.equals(obj);
	}
}
equals()的重写规则之必要性深入解读
public class AbnormalResult {
	public static void main(String[] args) {
		List<A> list =new ArrayList<A>();
		A a = new A();
		B b = new B();
		
		list.add(a);
		System.out.println("list.contains(a):" + list.contains(a));
		System.out.println("list.contains(b):" + list.contains(b));
		list.clear();
		System.out.println("--------------------------------------------------------after clear------------------------------");
		System.out.println("list.contains(a):" + list.contains(a));
		System.out.println("list.contains(b):" + list.contains(b));
		list.add(b);
		System.out.println("--------------------------------------------after clear------------------add(b)------------------------");
		
		System.out.println("list.contains(a):" + list.contains(a));
		System.out.println("list.contains(b):" + list.contains(b));
		
	}
	static class A{
		public boolean equals(Object obj) {
			return obj instanceof A;
		}
	}
	
	static class B extends A {
		public boolean equals (Object obj) {
			return obj instanceof B;
		}
	}
 }

程序输出结果:

list.contains(a)true
list.contains(b)false
--------------------------------------------------------after clear------------------------------
list.contains(a)false
list.contains(b)false
--------------------------------------------after clear------------------add(b)------------------------
list.contains(a)true
list.contains(b)true

上述代码违反了对称性。主要是看ArrayList的contains()方法的实现过程就理解了为什么会出现上述情况

    public boolean contains(Object o) {
	return indexOf(o) >= 0;
    }

    /**
     * Returns the index of the first occurrence of the specified element
     * in this list, or -1 if this list does not contain the element.
     * More formally, returns the lowest index <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
     * or -1 if there is no such index.
     * 如果返回-1就说明不包含这个元素。该方法的核心就是使用equals方法进行比较
     */
    public int indexOf(Object o) {
	if (o == null) {
	    for (int i = 0; i < size; i++)
		if (elementData[i]==null)
		    return i;
	} else {
	    for (int i = 0; i < size; i++)
		if (o.equals(elementData[i]))
		    return i;
	}
	return -1;
    }

结果为true,equals方法也就会返回true,这就是整个过程。但很明显结果是有问题的,因为我们的 list的泛型是A,而B又继承了A,此时无论加入了a还是b,都属于同种类型,所以无论是contains(a),还是contains(b)都应该返回true才算正常。而最终却出现上面的结果,这就是因为重写equals方法时没遵守对称性原则导致的结果,如果没遵守传递性也同样会造成上述的结果。当然这里的解决方法也比较简单,我们只要将B类的equals方法修改一下就可以了。

static class B extends A{  
        @Override  
        public boolean equals(Object obj) {  
            if(obj instanceof B){  
                return true;  
            }  
            return super.equals(obj);  
        }  
    }  
重写equals方法时一定要重写hashCode方法

我们知道在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等,对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。因此到这里我们就明白了,如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定,这样的话,如果我们通过映射表(Map接口)操作相关对象时,就无法达到我们预期想要的效果。下列例子来自:《java深入解析》

import java.util.HashMap;
import java.util.Map;
public class MapTest {
	public static void main(String[] args) {
		Map<String,Value> map1 = new HashMap<String,Value>();
		String s1 = new String("key");
		String s2 = new String("key");	
		Value value = new Value(2);
		map1.put(s1, value);
		System.out.println("s1.equals(s2):"+s1.equals(s2));
		System.out.println("map1.get(s1):"+map1.get(s1));
		System.out.println("map1.get(s2):"+map1.get(s2));
		
		
		Map<Key,Value> map2 = new HashMap<Key,Value>();
		Key k1 = new Key("A");
		Key k2 = new Key("A");
		map2.put(k1, value);
		System.out.println("k1.equals(k2):"+s1.equals(s2));
		System.out.println("map2.get(k1):"+map2.get(k1));
		System.out.println("map2.get(k2):"+map2.get(k2));
	}
	
	/**
	 * 键
	 * @author zejian
	 *
	 */
	static class Key{
		private String k;
		public Key(String key){
			this.k=key;
		}
		
		@Override
		public boolean equals(Object obj) {
			if(obj instanceof Key){
				Key key=(Key)obj;
				return k.equals(key.k);
			}
			return false;
		}
	}
	
	/**
	 * 值
	 * @author zejian
	 *
	 */
	static class Value{
		private int v;
		
		public Value(int v){
			this.v=v;
		}
		
		@Override
		public String toString() {
			return "类Value的值-->"+v;
		}
	}
}

控制台输出结果:

s1.equals(s2):true  
map1.get(s1):类Value的值-->2  
map1.get(s2):类Value的值-->2  
k1.equals(k2):true  
map2.get(k1):类Value的值-->2  
map2.get(k2):null  

对于s1和s2的结果,我们并不惊讶,因为相同的内容的s1和s2获取相同内的value这个很正常,因为String类重写了equals方法和hashCode方法,使其比较的是内容和获取的是内容的哈希码。但是对于k1和k2的结果就不太尽人意了,k1获取到的值是2,k2获取到的是null,这是为什么呢?想必大家已经发现了,Key只重写了equals方法并没有重写hashCode方法,这样的话,equals比较的确实是内容,而hashCode方法呢?没重写,那就肯定调用超类Object的hashCode方法,这样返回的不就是地址了吗?k1与k2属于两个不同的对象,返回的地址肯定不一样,所以现在我们知道调用map2.get(k2)为什么返回null了吧?那么该如何修改呢?很简单,我们要做也重写一下hashCode方法即可(如果参与equals方法比较的成员变量是引用类型的,则可以递归调用hashCode方法来实现):

@Override  
public int hashCode() {  
     return k.hashCode();  
}

再次运行:

s1.equals(s2):true  
map1.get(s1):类Value的值-->2  
map1.get(s2):类Value的值-->2  
k1.equals(k2):true  
map2.get(k1):类Value的值-->2  
map2.get(k2):类Value的值-->2  
编写一个完美的equals()方法的几点建议
  • 显示参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量(参数名命名,强制类型转换,转换成当前类)
  • 第一步:检测this和otherObject是否引用同一个对象,如果引用同一个对象直接返回true。if(this == otherObject) return true;
  • 第二步:检测this和otherObject是否为null,如果是null,直接返回false。if(objectOther == null) return false;
  • 第三步:比较this和otherObject是否属于同一个类
    • 如果equals的语义在每个子类中有所改变,就使用getClass进行检测。if(getClass()!=otherObject.getClass()) return false;
    • 如果所有的子类都有统一的语义,就可以使用instanceof检查:if(!(otherObject instanceof ClassName)) return flase
  • 将otherObject转换为相应的类类型变量:ClassName other = (ClassName)otherObject;
  • 现在开始对所有需要比较的域开始比较。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,返回true,否则返回false;
  • 如果子类中重新定义了equals方法,就要在其中包含super.equals(other);

5. protected native Object clone( ) throws CloneNotSupportedException方法

Object类的clone方法是浅拷贝

下面的讲解内容摘抄自:http://ihenu.iteye.com/blog/2233249

  • clode()方法又是一个被声明为native的方法,因此,我们知道了clone()方法并不是Java的原生方法,具体的实现是有C/C++完成
  • clone英文翻译为"克隆",其目的是创建并返回此对象的一个副本。
  • Java术语表述为:clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。

首先看一下下面的这个例子:

import com.corn.Person;  
  
public class ObjectTest {  
  
    public static void main(String[] args) {  
  
        Object o1 = new Object();  
        // The method clone() from the type Object is not visible  
        Object clone = o1.clone();  
    }  
  
}  

例子很简单,在main()方法中,new一个Oject对象后,想直接调用此对象的clone方法克隆一个对象,但是出现错误提示:“The method clone() from the type Object is not visible”

why? 根据提示,第一反应是ObjectTest类中定义的Oject对象无法访问其clone()方法。回到Object类中clone()方法的定义,可以看到其被声明为protected,估计问题就在这上面了,protected修饰的属性或方法表示:在同一个包内或者不同包的子类可以访问。显然,Object类与ObjectTest类在不同的包中,但是ObjectTest继承自Object,是Object类的子类,于是,现在却出现子类中通过Object引用不能访问protected方法,原因在于对"不同包中的子类可以访问"没有正确理解。

“不同包中的子类可以访问”,是指当两个类不在同一个包中的时候,继承自父类的子类内部且主调(调用者)为子类的引用时才能访问父类用protected修饰的成员(属性/方法)。 在子类内部,主调为父类的引用时并不能访问此protected修饰的成员。!(super关键字除外)

于是,上例改成如下形式,我们发现,可以正常编译:

public class ObjectTest {  
  
    public static void main(String[] args) {  
        ObjectTest ot1 = new ObjectTest();  
  
        try {  
            ObjectTest ot2 = (ObjectTest) ot1.clone();  
        } catch (CloneNotSupportedException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
    }  
  
}  

是的,因为此时的主调已经是子类的引用了。
上述代码在运行过程中会抛出"java.lang.CloneNotSupportedException",表明clone()方法并未正确执行完毕,问题的原因在与Java中的语法规定:

clone()的正确调用是需要实现Cloneable接口,如果没有实现Cloneable接口,并且子类直接调用Object类的clone()方法,则会抛出CloneNotSupportedException异常。

Cloneable接口仅是一个表示接口,接口本身不包含任何方法,用来指示Object.clone()可以合法的被子类引用所调用。
于是,上述代码改成如下形式,即可正确指定clone()方法以实现克隆。

public class ObjectTest implements Cloneable {  
  
    public static void main(String[] args) {  
  
        ObjectTest ot1 = new ObjectTest();  
  
        try {  
            ObjectTest ot2 = (ObjectTest) ot1.clone();  
            System.out.println("ot2:" + ot2);  
        } catch (CloneNotSupportedException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
    }  
  
}  

总结:

  • 1. Obejct类的clone()方法实现的是浅拷贝
  • 2. 在子类内部,主调为父类的引用时并不能访问此protected修饰的成员,(supper关键字除外)。调用者为子类的引用时才能访问父类用protected修饰的成员
  • 3. 想要在子类中调用父类的clone()方法,需要实现Cloneable接口,该接口用来指示Object.clone()可以合法的被子类引用所调用

6. public String toString()方法

Object类中该方法的源码:返回该对象的字符串表示。

 public String toString() {  
    return getClass().getName() + "@" + Integer.toHexString(hashCode());  
}  
  • 当使用System.out.println(Object obj);时,返回的就是该obj对象的toString方法,实际上System.out.println()内部是通过toString实现的
  • getClass()返回对象的类对象,getClassName()以String形式返回类对象的名称(含包名)。Integer.toHexString(hashCode())则是以对象的哈希码为实参,以16进制无符号整数形式返回此哈希码的字符串表示形式。
  • 如u1对象的哈希码是638,则对应的16进制为27e,调用toString()方法返回的结果为:com.corn.User@27e。包名.类名@哈希码
  • 因此:toString()是由对象的类型和其哈希码唯一确定,同一类型但不相等的两个对象分别调用toString()方法返回的结果可能相同。

7. protected void finalize() throws Throwable 方法

该方法与垃圾回收机制有关

我们发现Object类中finalize方法被定义成一个空方法,为什么要如此定义呢?finalize方法的调用时机是怎么样的呢?

首先,Object中定义finalize方法表明Java中每一个对象都将具有finalize这种行为,其具体调用时机在:JVM准备对此对形象所占用的内存空间进行垃圾回收前,将被调用。由此可以看出,此方法并不是由我们主动去调用的(虽然可以主动去调用,此时与其他自定义方法无异)。

一个对象的finalize()方法只会被调用一次,而且finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),产生问题。
一般来说,不推荐使用finalize()方法,它跟析构函数不一样。


8.notify/notifyAll/wait/。。剩下的方法待更。。。。。

本文摘抄自:
http://ihenu.iteye.com/blog/2233249
http://www.cnblogs.com/binyue/p/3434918.html
https://docs.oracle.com/javase/8/docs/api/
http://blog.csdn.net/javazejian/article/details/51348320

发布了397 篇原创文章 · 获赞 71 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/xiaojie_570/article/details/79204475