Most visited

Recently visited

Added in API level 1

ObjectInputStream

public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants

java.lang.Object
   ↳ java.io.InputStream
     ↳ java.io.ObjectInputStream


ObjectInputStream将先前使用ObjectOutputStream编写的原始数据和对象进行反序列化。

当分别与FileOutputStream和FileInputStream一起使用时,ObjectOutputStream和ObjectInputStream可以为应用程序提供持久存储的图形对象。 ObjectInputStream用于恢复先前序列化的那些对象。 其他用途包括使用套接字流在主机之间传递对象或在远程通信系统中编组和解组参数和参数。

ObjectInputStream确保根据流创建的图中的所有对象的类型与Java虚拟机中存在的类匹配。 类使用标准机制根据需要加载。

只能从流中读取支持java.io.Serializable或java.io.Externalizable接口的对象。

方法readObject用于从流中读取对象。 应该使用Java的安全转换来获得所需的类型。 在Java中,字符串和数组是对象,在序列化过程中被视为对象。 阅读时,需要将其转换为预期的类型。

可以使用DataInput上适当的方法从流中读取原始数据类型。

对象的默认反序列化机制将每个字段的内容恢复为其写入时的值和类型。 声明为瞬态或静态的字段被反序列化过程忽略。 对其他对象的引用会根据需要从流中读取这些对象。 使用参考共享机制可以正确恢复对象的图形。 反序列化时总是分配新对象,这可以防止覆盖现有对象。

读对象类似于运行新对象的构造函数。 内存分配给对象并初始化为零(NULL)。 对于不可序列化的类调用No-arg构造函数,然后可以从流中恢复可序列化类的字段,该流从与java.lang.object最接近的可序列化类开始,并以对象的最具体类完成。

例如,要从ObjectOutputStream中的示例所写的流中读取数据:

      FileInputStream fis = new FileInputStream("t.tmp");
      ObjectInputStream ois = new ObjectInputStream(fis);

      int i = ois.readInt();
      String today = (String) ois.readObject();
      Date date = (Date) ois.readObject();

      ois.close();
 

类通过实现java.io.Serializable或java.io.Externalizable接口来控制它们如何被序列化。

实现Serializable接口允许对象序列化来保存和恢复对象的整个状态,并允许类在写入数据流和读取数据的时间之间进化。 它会自动遍历对象之间的引用,保存并恢复整个图形。

在序列化和反序列化过程中需要特殊处理的可序列化类应实现以下方法:

 private void writeObject(java.io.ObjectOutputStream stream)
     throws IOException;
 private void readObject(java.io.ObjectInputStream stream)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;
 

readObject方法负责使用由相应的writeObject方法写入流的数据读取和恢复其特定类的对象状态。 该方法不需要关注属于其超类或亚类的状态。 通过从各个字段的ObjectInputStream中读取数据并分配给对象的相应字段来恢复状态。 DataInput支持读取原始数据类型。

任何尝试读取超出相应writeObject方法写入的自定义数据边界的对象数据都将导致抛出一个OptionalDataException,其中eof字段值为true。 超出分配数据末尾的非对象读取将以与它们指示流结束的方式相同的方式反映数据的结尾:按字节读取或读取字节数返回-1,而字节读取将返回-1读取操作将抛出EOFException。 如果没有相应的writeObject方法,则默认序列化数据的末尾标记分配数据的结束。

从readExternal方法中发出的原始和对象读取调用的行为方式相同 - 如果流已经位于由相应的writeExternal方法写入的数据的末尾,则对象读取将抛出OptionalDataExceptions,并将eof设置为true,按字节读取将返回-1,原始读取将抛出EOFException。 请注意,这种行为不适用于使用旧的ObjectStreamConstants.PROTOCOL_VERSION_1协议编写的流,其中由writeExternal方法写入的数据末尾没有划分,因此无法检测到。

如果序列化流没有将给定的类列为被反序列化的对象的超类,则readObjectNoData方法负责初始化其特定类的对象状态。 如果接收方使用与发送方不同的反序列化实例类的不同版本,并且接收方的版本扩展了未由发送方版本扩展的类,则可能会发生这种情况。 如果序列化流已被篡改,也可能发生这种情况; 因此,尽管存在“敌对”或不完整的源码流,但readObjectNoData可用于正确初始化反序列化的对象。

序列化不读取或赋值给任何没有实现java.io.Serializable接口的对象的字段。 不可序列化的对象的子类可以是可序列化的。 在这种情况下,不可序列化的类必须有一个无参数构造函数来允许其字段被初始化。 在这种情况下,保存和恢复不可序列化类的状态是子类的责任。 通常情况下,该类的字段是可访问的(public,package或protected),或者有可用于恢复状态的get和set方法。

反序列化对象时发生的任何异常都将被ObjectInputStream捕获并中止读取过程。

实现Externalizable接口允许对象完全控制对象序列化表单的内容和格式。 调用Externalizable接口的方法writeExternal和readExternal来保存和恢复对象状态。 当由类实现时,他们可以使用ObjectOutput和ObjectInput的所有方法编写和读取自己的状态。 对象负责处理发生的任何版本控制。

枚举常量的反序列化与普通的可序列化或可外部化的对象不同。 枚举常量的序列化形式仅由其名称组成; 不传送常量的字段值。 为了反序列化一个枚举常量,ObjectInputStream从流中读取常量名称; 然后通过使用枚举常量的基类型和接收到的常量名称作为参数调用静态方法Enum.valueOf(Class, String)获得反序列化的常量。 像其他可序列化或可外部化的对象一样,枚举常量可以用作后续出现在序列化流中的后向引用的目标。 枚举常量反序列化的过程无法自定义:在反序列化过程中忽略由枚举类型定义的任何特定于类的readObject,readObjectNoData和readResolve方法。 同样,也会忽略任何serialPersistentFields或serialVersionUID字段声明 - 所有枚举类型都有一个固定的serialVersionUID 0L。

也可以看看:

Summary

Nested classes

class ObjectInputStream.GetField

提供对从输入流中读取的持久字段的访问。

Inherited constants

From interface java.io.ObjectStreamConstants

Inherited fields

From interface java.io.ObjectStreamConstants

Public constructors

ObjectInputStream(InputStream in)

创建一个从指定的InputStream读取的ObjectInputStream。

Protected constructors

ObjectInputStream()

为完全重新实现ObjectInputStream的子类提供一种方法,使其不必分配此ObjectInputStream实现所使用的私有数据。

Public methods

int available()

返回可以不阻塞地读取的字节数。

void close()

关闭输入流。

void defaultReadObject()

从这个流中读取当前类的非静态和非瞬态字段。

int read()

读取一个字节的数据。

int read(byte[] buf, int off, int len)

读入一个字节数组。

boolean readBoolean()

读入一个布尔值。

byte readByte()

读取一个8位字节。

char readChar()

读取一个16位字符。

double readDouble()

读取64位双精度。

ObjectInputStream.GetField readFields()

从流中读取持久字段并使其可以按名称使用。

float readFloat()

读取32位浮点数。

void readFully(byte[] buf)

读取字节,阻塞直到读取所有字节。

void readFully(byte[] buf, int off, int len)

读取字节,阻塞直到读取所有字节。

int readInt()

读取32位int。

String readLine()

此方法在API级别1中已弃用。此方法不会将字节正确转换为字符。 有关详细信息和备选方案,请参阅DataInputStream。

long readLong()

读取64位长。

final Object readObject()

从ObjectInputStream中读取一个对象。

short readShort()

读取16位短。

String readUTF()

modified UTF-8格式读取字符串。

Object readUnshared()

从ObjectInputStream中读取一个“非共享”对象。

int readUnsignedByte()

读取一个无符号的8位字节。

int readUnsignedShort()

读取一个无符号的16位短路。

void registerValidation(ObjectInputValidation obj, int prio)

在返回图之前注册要验证的对象。

int skipBytes(int len)

跳过字节。

Protected methods

boolean enableResolveObject(boolean enable)

启用流以允许替换从流中读取的对象。

ObjectStreamClass readClassDescriptor()

从序列化流中读取类描述符。

Object readObjectOverride()

此方法由ObjectOutputStream的受信任子类调用,它使用受保护的无参数构造函数构造ObjectOutputStream。

void readStreamHeader()

readStreamHeader方法用于允许子类读取和验证其自己的流标头。

Class<?> resolveClass(ObjectStreamClass desc)

加载与指定的流类描述等效的本地类。

Object resolveObject(Object obj)

此方法将允许ObjectInputStream的可信子类在反序列化过程中将一个对象替换为另一个对象。

Class<?> resolveProxyClass(String[] interfaces)

返回实现代理类描述符中指定的接口的代理类; 子类可以实现此方法从流中读取自定义数据以及动态代理类的描述符,从而允许它们为接口和代理类使用备用加载机制。

Inherited methods

From class java.io.InputStream
From class java.lang.Object
From interface java.io.Closeable
From interface java.io.ObjectInput
From interface java.lang.AutoCloseable
From interface java.io.DataInput

Public constructors

ObjectInputStream

Added in API level 1
ObjectInputStream (InputStream in)

创建一个从指定的InputStream读取的ObjectInputStream。 从流中读取序列化流头并进行验证。 该构造函数将阻塞,直到相应的ObjectOutputStream写入并刷新标题。

如果安装了安全管理器,则此构造函数将在覆盖ObjectInputStream.readFields或ObjectInputStream.readUnshared方法的子类的构造函数直接或间接调用时检查“enableSubclassImplementation”SerializablePermission。

Parameters
in InputStream: input stream to read from
Throws
StreamCorruptedException if the stream header is incorrect
IOException if an I/O error occurs while reading stream header
SecurityException if untrusted subclass illegally overrides security-sensitive methods
NullPointerException if in is null

也可以看看:

Protected constructors

ObjectInputStream

Added in API level 1
ObjectInputStream ()

为完全重新实现ObjectInputStream的子类提供一种方法,使其不必分配此ObjectInputStream实现所使用的私有数据。

如果安装了安全管理器,则此方法首先使用 SerializablePermission("enableSubclassImplementation")权限调用安全管理器的 checkPermission方法,以确保启用子类化操作即可。

Throws
SecurityException if a security manager exists and its checkPermission method denies enabling subclassing.
IOException

也可以看看:

Public methods

available

Added in API level 1
int available ()

返回可以不阻塞地读取的字节数。

Returns
int the number of available bytes.
Throws
IOException if there are I/O errors while reading from the underlying InputStream

close

Added in API level 1
void close ()

关闭输入流。 必须调用才能释放与流关联的所有资源。

Throws
IOException If an I/O error has occurred.

defaultReadObject

Added in API level 1
void defaultReadObject ()

从这个流中读取当前类的非静态和非瞬态字段。 这只能从被反序列化的类的readObject方法中调用。 如果它被调用,它会抛出NotActiveException。

Throws
ClassNotFoundException if the class of a serialized object could not be found.
IOException if an I/O error occurs.
NotActiveException if the stream is not currently reading objects.

read

Added in API level 1
int read ()

读取一个字节的数据。 如果没有可用输入,此方法将被阻止。

Returns
int the byte read, or -1 if the end of the stream is reached.
Throws
IOException If an I/O error has occurred.

read

Added in API level 1
int read (byte[] buf, 
                int off, 
                int len)

读入一个字节数组。 此方法将阻止,直到有些输入可用。 考虑使用java.io.DataInputStream.readFully读取'长度'字节。

Parameters
buf byte: the buffer into which the data is read
off int: the start offset of the data
len int: the maximum number of bytes read
Returns
int the actual number of bytes read, -1 is returned when the end of the stream is reached.
Throws
IOException If an I/O error has occurred.

也可以看看:

readBoolean

Added in API level 1
boolean readBoolean ()

读入一个布尔值。

Returns
boolean the boolean read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readByte

Added in API level 1
byte readByte ()

读取一个8位字节。

Returns
byte the 8 bit byte read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readChar

Added in API level 1
char readChar ()

读取一个16位字符。

Returns
char the 16 bit char read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readDouble

Added in API level 1
double readDouble ()

读取64位双精度。

Returns
double the 64 bit double read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readFields

Added in API level 1
ObjectInputStream.GetField readFields ()

从流中读取持久字段并使其可以按名称使用。

Returns
ObjectInputStream.GetField the GetField object representing the persistent fields of the object being deserialized
Throws
ClassNotFoundException if the class of a serialized object could not be found.
IOException if an I/O error occurs.
NotActiveException if the stream is not currently reading objects.

readFloat

Added in API level 1
float readFloat ()

读取32位浮点数。

Returns
float the 32 bit float read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readFully

Added in API level 1
void readFully (byte[] buf)

读取字节,阻塞直到读取所有字节。

Parameters
buf byte: the buffer into which the data is read
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readFully

Added in API level 1
void readFully (byte[] buf, 
                int off, 
                int len)

读取字节,阻塞直到读取所有字节。

Parameters
buf byte: the buffer into which the data is read
off int: the start offset of the data
len int: the maximum number of bytes to read
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readInt

Added in API level 1
int readInt ()

读取32位int。

Returns
int the 32 bit integer read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readLine

Added in API level 1
String readLine ()

此方法在API级别1中已弃用。
此方法不能正确地将字节转换为字符。 有关详细信息和备选方案,请参阅DataInputStream。

读入已由\ n,\ r,\ r \ n或EOF终止的行。

Returns
String a String copy of the line.
Throws
IOException if there are I/O errors while reading from the underlying InputStream

readLong

Added in API level 1
long readLong ()

读取64位长。

Returns
long the read 64 bit long.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readObject

Added in API level 1
Object readObject ()

从ObjectInputStream中读取一个对象。 读取对象的类,类的签名以及该类及其所有超类型的非瞬态和非静态字段的值。 使用writeObject和readObject方法可以覆盖类的默认反序列化。 此对象引用的对象是传递式读取的,以便通过readObject重建对象的完全等效图形。

当其所有字段及其引用的对象完全恢复时,根对象完全恢复。 此时,对象验证回调会根据其注册的优先级顺序执行。 回调由对象注册(在readObject特殊方法中),因为它们分别被恢复。

针对InputStream和不应该反序列化的类的问题引发了异常。 所有异常对于InputStream都是致命的,并使其处于不确定状态; 调用者可以忽略或恢复流状态。

Returns
Object the object read from the stream
Throws
ClassNotFoundException Class of a serialized object cannot be found.
InvalidClassException Something is wrong with a class used by serialization.
StreamCorruptedException Control information in the stream is inconsistent.
OptionalDataException Primitive data was found in the stream instead of objects.
IOException Any of the usual Input/Output related exceptions.

readShort

Added in API level 1
short readShort ()

读取16位短。

Returns
short the 16 bit short read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readUTF

Added in API level 1
String readUTF ()

modified UTF-8格式读取字符串。

Returns
String the String.
Throws
IOException if there are I/O errors while reading from the underlying InputStream
UTFDataFormatException if read bytes do not represent a valid modified UTF-8 encoding of a string

readUnshared

Added in API level 1
Object readUnshared ()

从ObjectInputStream中读取一个“非共享”对象。 此方法与readObject相同,只是它阻止对readObject的后续调用,并且readUnshared不会返回通过此调用获取的反序列化实例的其他引用。 特别:

  • If readUnshared is called to deserialize a back-reference (the stream representation of an object which has been written previously to the stream), an ObjectStreamException will be thrown.
  • If readUnshared returns successfully, then any subsequent attempts to deserialize back-references to the stream handle deserialized by readUnshared will cause an ObjectStreamException to be thrown.
Deserializing an object via readUnshared invalidates the stream handle associated with the returned object. Note that this in itself does not always guarantee that the reference returned by readUnshared is unique; the deserialized object may define a readResolve method which returns an object visible to other parties, or readUnshared may return a Class object or enum constant obtainable elsewhere in the stream or through external means. If the deserialized object defines a readResolve method and the invocation of that method returns an array, then readUnshared returns a shallow clone of that array; this guarantees that the returned array object is unique and cannot be obtained a second time from an invocation of readObject or readUnshared on the ObjectInputStream, even if the underlying data stream has been manipulated.

重载此方法的ObjectInputStream子类只能在拥有“enableSubclassImplementation”SerializablePermission; 任何尝试实例化没有此权限的子类都将导致抛出SecurityException。

Returns
Object reference to deserialized object
Throws
ClassNotFoundException if class of an object to deserialize cannot be found
StreamCorruptedException if control information in the stream is inconsistent
ObjectStreamException if object to deserialize has already appeared in stream
OptionalDataException if primitive data is next in stream
IOException if an I/O error occurs during deserialization

readUnsignedByte

Added in API level 1
int readUnsignedByte ()

读取一个无符号的8位字节。

Returns
int the 8 bit byte read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

readUnsignedShort

Added in API level 1
int readUnsignedShort ()

读取一个无符号的16位短路。

Returns
int the 16 bit short read.
Throws
EOFException If end of file is reached.
IOException If other I/O error has occurred.

registerValidation

Added in API level 1
void registerValidation (ObjectInputValidation obj, 
                int prio)

在返回图之前注册要验证的对象。 虽然与resolveObject类似,但这些验证在整个图形重新构建后调用。 通常情况下,readObject方法会将对象注册到流中,以便当所有对象都恢复时,可以执行最后一组验证。

Parameters
obj ObjectInputValidation: the object to receive the validation callback.
prio int: controls the order of callbacks;zero is a good default. Use higher numbers to be called back earlier, lower numbers for later callbacks. Within a priority, callbacks are processed in no particular order.
Throws
NotActiveException The stream is not currently reading objects so it is invalid to register a callback.
InvalidObjectException The validation object is null.

skipBytes

Added in API level 1
int skipBytes (int len)

跳过字节。

Parameters
len int: the number of bytes to be skipped
Returns
int the actual number of bytes skipped.
Throws
IOException If an I/O error has occurred.

Protected methods

enableResolveObject

Added in API level 1
boolean enableResolveObject (boolean enable)

启用流以允许替换从流中读取的对象。 启用后,将为每个正在反序列化的对象调用resolveObject方法。

如果 启用为真,并且安装了安全管理器,则此方法首先使用 SerializablePermission("enableSubstitution")权限调用安全管理器的 checkPermission方法,以确保允许流允许替换从流中读取的对象。

Parameters
enable boolean: true for enabling use of resolveObject for every object being deserialized
Returns
boolean the previous setting before this method was invoked
Throws
SecurityException if a security manager exists and its checkPermission method denies enabling the stream to allow objects read from the stream to be replaced.

也可以看看:

readClassDescriptor

Added in API level 1
ObjectStreamClass readClassDescriptor ()

从序列化流中读取类描述符。 当ObjectInputStream需要类描述符作为序列化流中的下一项时调用此方法。 ObjectInputStream的子类可以重写此方法以读取已用非标准格式(通过ObjectOutputStream的子类重写了writeClassDescriptor方法)编写的类描述符。 默认情况下,此方法根据对象序列化规范中定义的格式读取类描述符。

Returns
ObjectStreamClass the class descriptor read
Throws
IOException If an I/O error has occurred.
ClassNotFoundException If the Class of a serialized object used in the class descriptor representation cannot be found

也可以看看:

readObjectOverride

Added in API level 1
Object readObjectOverride ()

此方法由ObjectOutputStream的受信任子类调用,它使用受保护的无参数构造函数构造ObjectOutputStream。 该子类预计会提供修饰符“final”的重写方法。

Returns
Object the Object read from the stream.
Throws
ClassNotFoundException Class definition of a serialized object cannot be found.
OptionalDataException Primitive data was found in the stream instead of objects.
IOException if I/O errors occurred while reading from the underlying stream

也可以看看:

readStreamHeader

Added in API level 1
void readStreamHeader ()

readStreamHeader方法用于允许子类读取和验证其自己的流标头。 它读取并验证幻数和版本号。

Throws
IOException if there are I/O errors while reading from the underlying InputStream
StreamCorruptedException if control information in the stream is inconsistent

resolveClass

Added in API level 1
Class<?> resolveClass (ObjectStreamClass desc)

加载与指定的流类描述等效的本地类。 子类可以实现此方法,以允许从替代源获取类。

ObjectOutputStream的相应方法是annotateClass 对于流中的每个唯一类,该方法只会被调用一次。 该方法可以由子类实现以使用替代的加载机制,但必须返回Class对象。 一旦返回,如果该类不是数组类,则将其serialVersionUID与序列化类的serialVersionUID进行比较,如果不匹配,则反序列化将失败并引发InvalidClassException

ObjectInputStream中此方法的默认实现返回调用的结果

     Class.forName(desc.getName(), false, loader)
 
where loader is determined as follows: if there is a method on the current thread's stack whose declaring class was defined by a user-defined class loader (and was not a generated to implement reflective invocations), then loader is class loader corresponding to the closest such method to the currently executing frame; otherwise, loader is null. If this call results in a ClassNotFoundException and the name of the passed ObjectStreamClass instance is the Java language keyword for a primitive type or void, then the Class object representing that primitive type or void will be returned (e.g., an ObjectStreamClass with the name "int" will be resolved to Integer.TYPE). Otherwise, the ClassNotFoundException will be thrown to the caller of this method.

Parameters
desc ObjectStreamClass: an instance of class ObjectStreamClass
Returns
Class<?> a Class object corresponding to desc
Throws
IOException any of the usual Input/Output exceptions.
ClassNotFoundException if class of a serialized object cannot be found.

resolveObject

Added in API level 1
Object resolveObject (Object obj)

此方法将允许ObjectInputStream的可信子类在反序列化过程中将一个对象替换为另一个对象。 在调用enableResolveObject之前,替换对象将被禁用。 enableResolveObject方法检查请求解析对象的流是否可信。 对可序列化对象的每个引用都传递给resolveObject。 为了确保对象的私有状态不是无意暴露的,只有受信任的流可以使用resolveObject。

该方法在读取对象之后但在从readObject返回之前调用。 默认的resolveObject方法只返回相同的对象。

当一个子类正在替换对象时,它必须确保被替换的对象与引用将被存储的每个字段兼容。 对象的类型不是字段或数组元素的类型的子类,通过引发异常来中止序列化,并且不存储该对象。

当每个对象第一次遇到时,这个方法只被调用一次。 所有后续对该对象的引用都将被重定向到新对象。

Parameters
obj Object: object to be substituted
Returns
Object the substituted object
Throws
IOException Any of the usual Input/Output exceptions.

resolveProxyClass

Added in API level 1
Class<?> resolveProxyClass (String[] interfaces)

返回实现代理类描述符中指定的接口的代理类; 子类可以实现此方法从流中读取自定义数据以及动态代理类的描述符,从而允许它们为接口和代理类使用备用加载机制。

对于流中的每个唯一代理类描述符,该方法只会被调用一次。

ObjectOutputStream的相应方法是annotateProxyClass 对于给定的子类ObjectInputStream重写该方法中, annotateProxyClass中的相应子类方法ObjectOutputStream必须写由该方法读取的任何数据或对象。

此方法在ObjectInputStream的默认实现返回调用Proxy.getProxyClass的结果和interfaces参数中指定的接口的Class对象的列表。 每个接口名称iClass对象是通过调用返回的值

     Class.forName(i, false, loader)
 
where loader is that of the first non- null class loader up the execution stack, or null if no non- null class loaders are on the stack (the same class loader choice used by the resolveClass method). Unless any of the resolved interfaces are non-public, this same value of loader is also the class loader passed to Proxy.getProxyClass; if non-public interfaces are present, their class loader is passed instead (if more than one non-public interface class loader is encountered, an IllegalAccessError is thrown). If Proxy.getProxyClass throws an IllegalArgumentException, resolveProxyClass will throw a ClassNotFoundException containing the IllegalArgumentException.

Parameters
interfaces String: the list of interface names that were deserialized in the proxy class descriptor
Returns
Class<?> a proxy class for the specified interfaces
Throws
IOException any exception thrown by the underlying InputStream
ClassNotFoundException if the proxy class or any of the named interfaces could not be found

也可以看看:

Hooray!